docs: add tutorials for custom filters and tags

This commit is contained in:
Harttle
2022-01-22 18:52:37 +08:00
committed by Harttle
parent 371da3da74
commit 394d5a1ae2
10 changed files with 453 additions and 1 deletions
+3
View File
@@ -9,6 +9,9 @@ tutorials:
advanced:
caching: caching.html
registeration: register-filters-tags.html
access_scope_in_filters: access-scope-in-filters.html
parse_parameters: parse-parameters.html
sync_and_async: sync-and-async.html
whitespace: whitespace-control.html
plugins: plugins.html
operators: operators.html
@@ -0,0 +1,32 @@
---
title: Access Scope in Filters
---
As covered in [Register Filters/Tags][register-filters], we can access filter arguments directly in filter function like:
```javascript
// Usage: {{ 1 | add: 2, 3 }}
// Output: 6
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
```
When it comes to stateful filters, for example transform a URL path to full URL, we'll need to access a `origin` in current scope:
```javascript
// Usage: {{ '/index.html' | fullURL }}
// Scope: { origin: "https://liquidjs.com" }
// Output: https://liquidjs.com/index.html
engine.registerFilter('fullURL', function (path) {
const origin = this.context.get(['origin'])
return new URL(path, origin).toString()
})
```
See this JSFiddle: <http://jsfiddle.net/ctj364up/1/>
{% note warn Arrow Functions %}
<code>this</code> in arrow functions is bound to current JavaScript context, you'll need to use <code>function(){}</code> instead of <code>()=>{}</code> syntax to access <code>this.context</code> correctly.
{% endnote %}
[register-filters]: /tutorials/register-filters-tags.html
+100
View File
@@ -0,0 +1,100 @@
---
title: Parse Parameters
---
## Access Raw Parameters
As covered in [Register Filters/Tags][register-tags], tag parameters is available on `tagToken.args` as a raw string. For example:
```javascript
// Usage: {% random foo bar coo %}
// Output: "foo", "bar" or "coo"
engine.registerTag('random', {
parse(tagToken) {
// tagToken.args === "foo bar coo"
this.items = tagToken.args.split(' ')
},
render(context, emitter) {
// get a random index
const index = Math.floor(this.items.length * Math.random())
// output that item
emitter.write(this.items[index])
}
})
```
Here's a JSFiddle version: <http://jsfiddle.net/ctj364up/2/>
## Parse Parameters as Values
Sometimes we need more dynamic tags and want to pass values to the custom tag instead of static strings. Variables in LiquidJS can be literal (string, number, etc.) or a variable from current context scope.
The following modified template also contains 3 values to random from, but they're values instead of static strings. The first one is string literal, second one is an identifier, third one is a property access sequence containing two identifiers.
```liquid
{% random "foo" bar obj.coo %}
```
It can be tricky to parse all these cases manually, but there's a [Tokenizer][Tokenizer] class in LiquidJS you can make use of.
```javascript
const { Liquid, Tokenizer, evalToken } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
const tokenizer = new Tokenizer(tagToken.args)
this.items = []
while (!tokenizer.end()) {
// here readValue() returns a LiteralToken or PropertyAccessToken
this.items.push(tokenizer.readValue())
}
},
* render(context, emitter) {
const index = Math.floor(this.items.length * Math.random())
const token = this.items[index]
// in LiquidJS, we use yield to wait for async call
const value = yield evalToken(token, context)
emitter.write(value)
}
})
```
Calling this tag in scope `{ bar: "bar", obj: { coo: "coo" } }` yields exactly the same result as the first example. See this JSFiddle: <http://jsfiddle.net/ctj364up/3/>
{% note info Async ans Promises %}
Async calls in LiquidJS are implemented by generators directly, for we can call generators in synchronous manner so this tag implementation is also valid for `renderSync()`, `parseAndRenderSync()`, `renderFileSync()`. If you need to await a promise in tag implementation, simply replace `await somePromise` with `yield somePromise` and keep `* render()` instead of `async render()` will do the trick. See <a href="/tutorials/sync-and-async.html">Sync and Async</a> for more details.
{% endnote %}
## Parse Key-Value Pairs as Named Parameters
Named parameters become very handy when there're optional parameters or lots of parameters, in which case the order of parameters is not important. This is exactly what [Hash][Hash] class is invented for.
```liquid
{% random from:2, to:max %}
```
In the above example, we're trying to generate a random number in the range [2, max]. We'll use `Hash` to parse `from` and `to` parameters.
```javascript
const { Liquid, Hash } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
// parse the parameters structure into `this.args`
this.args = new Hash(tagToken.args)
},
* render(context, emitter) {
// evaluate the parameters in `context`
const {from, to} = yield this.args.render(context)
const length = to - from + 1
const value = from + Math.floor(length * Math.random())
emitter.write(value)
}
})
```
Rendering `{% random from:2, to:max %}` in scope `{ max: 10 }` will generate a random number in the range [2, 10]. See this JSFiddle: <http://jsfiddle.net/ctj364up/4/>
[register-tags]: /tutorials/register-filters-tags.html
[Tokenizer]: /api/classes/parser_tokenizer_.tokenizer.html
[Hash]: /api/classes/template_tag_hash_.hash.html
@@ -61,4 +61,4 @@ function disabledFilter(name) {
}
}
engine.registerFilter('plus', disabledFilter('plus'));
```
```
+90
View File
@@ -0,0 +1,90 @@
---
title: Sync and Async
---
LiquidJS supports both sync and async evaluate, and can be used with Promises. To reuse the same set of tag/filter implementations in both sync and async, LiquidJS tags are implemented as generators.
## Sync and Async API
All major methods on [Liquid][Liquid] supports both sync and async. These methods return Promises:
- `render()`
- `renderFile()`
- `parseFile()`
- `parseAndRender()`
- `evalValue()`
The synchronous version of methods contains a `Sync` suffix:
- `renderSync()`
- `renderFileSync()`
- `parseFileSync()`
- `parseAndRenderSync()`
- `evalValueSync()`
## Implement Sync-Compatible Tags
### Requirements
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)`:
- Should not directly `return <Promise>`, and
- Should not be declared as `async`.
{% 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>.
{% endnote %}
### Await Promises
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.
```typescript
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args
},
* render: function(ctx: Context) {
// _evalValue will behave synchronously when called by synchronous API
// in which case `ctx.sync == true`
var str = yield this.liquid._evalValue(this.str, ctx)
return str.toUpperCase()
}
})
```
See this JSFiddle: <http://jsfiddle.net/ctj364up/6/>.
## Async-only Tags
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
import { TagToken, Context, Emitter, TopLevelToken, toPromise } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args; // name
},
render: async function(ctx: Context) {
var str = await toPromise(this.liquid._evalValue(this.str, ctx));
// Or use the alternate API that returns a Promise
// var str = await this.liquid.evalValue(this.str, ctx);
return str.toUpperCase()
}
});
```
See this JSFiddle: <http://jsfiddle.net/ctj364up/5/>.
[Liquid]: /api/classes/liquid_.liquid.html
[toPromise]: /api/modules/liquid_.html#toPromise
@@ -0,0 +1,32 @@
---
title: 过滤器里访问上下文
---
在 [注册过滤器和标签][register-filters] 里介绍过,可以在函数参数里直接获得过滤器的参数:
```javascript
// Usage: {{ 1 | add: 2, 3 }}
// Output: 6
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
```
但有些过滤器还需要访问当前上下文的变量,比如把 URL 路径转换为完整的 URL 时,需要访问上下文的 `origin` 变量:
```javascript
// Usage: {{ '/index.html' | fullURL }}
// Scope: { origin: "https://liquidjs.com" }
// Output: https://liquidjs.com/index.html
engine.registerFilter('fullURL', function (path) {
const origin = this.context.get(['origin'])
return new URL(path, origin).toString()
})
```
见这个 JSFiddle<http://jsfiddle.net/ctj364up/1/>。
{% note warn 箭头函数 %}
在箭头函数里 `this` 会绑定到当前 JavaScript 上下文,你需要用 `function(){}` 来替代 `()=>{}` 语法,才能正确地访问 `this.context`
{% endnote %}
[register-filters]: /tutorials/register-filters-tags.html
@@ -0,0 +1,100 @@
---
title: 参数解析
---
## 访问原始参数
在 [注册过滤器和标签][register-tags] 中提到,可以通过 `tagToken.args` 来得到标签的原始参数字符串。例如:
```javascript
// Usage: {% random foo bar coo %}
// Output: "foo", "bar" or "coo"
engine.registerTag('random', {
parse(tagToken) {
// tagToken.args === "foo bar coo"
this.items = tagToken.args.split(' ')
},
render(context, emitter) {
// get a random index
const index = Math.floor(this.items.length * Math.random())
// output that item
emitter.write(this.items[index])
}
})
```
见这个 JSFiddle<http://jsfiddle.net/ctj364up/2/>。
## 解析参数的值
除了静态的参数字符串之外,我们更希望把动态的值传递给标签。LiquidJS 中的值可以是字面量(字符串、数字等,也可以是当前上下文的变量。
下面是修改过的模板,也包含三个值用来随机。但它们表示的是值而不是静态的字符串。第一个是字符串字面量,第二个是标识符(表示变量),第三个是属性访问表达式,包含两个标识符。
```liquid
{% random "foo" bar obj.coo %}
```
解析这么多种情况会很麻烦,但 LiquidJS 提供了 [Tokenizer][Tokenizer] 类来处理这种情况。
```javascript
const { Liquid, Tokenizer, evalToken } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
const tokenizer = new Tokenizer(tagToken.args)
this.items = []
while (!tokenizer.end()) {
// here readValue() returns a LiteralToken or PropertyAccessToken
this.items.push(tokenizer.readValue())
}
},
* render(context, emitter) {
const index = Math.floor(this.items.length * Math.random())
const token = this.items[index]
// in LiquidJS, we use yield to wait for async call
const value = yield evalToken(token, context)
emitter.write(value)
}
})
```
用上下文 `{ bar: "bar", obj: { coo: "coo" } }` 来调用这个标签可以得到上第一个例子一样的效果。见这个 JSFiddle<http://jsfiddle.net/ctj364up/3/>.
{% note info 异步和 Promise %}
在 LiquidJS 里异步用生成器实现,这样同样一份标签的实现也可以用于同步的 API 比如 `renderSync()``parseAndRenderSync()``renderFileSync()`。如果要在标签实现里等待 Promise,只需要把 `await somePromise` 换成 `yield somePromise`,并保留 `* render()` 不要改成 `async render()`。更多细节请参考 <a href="/tutorials/sync-and-async.html">Sync and Async</a>。
{% endnote %}
## 把键值对解析为命名参数
当参数很多时或者有可选参数时,使用命名参数语法会很方便。这时参数由无序的键值对构成,LiquidJS 中的 [Hash][Hash] 类就是来处理这种情况的。
```liquid
{% random from:2, to:max %}
```
上面的例子用来产生 [2, max] 范围内的随机数。我们要用 `Hash` 来解析 `from``to` 参数。
```javascript
const { Liquid, Hash } = require('liquidjs')
engine.registerTag('random', {
parse(tagToken) {
// 解析参数结果,存到 `this.args` 里
this.args = new Hash(tagToken.args)
},
* render(context, emitter) {
// 在当前 `context` 下计算参数的值
const {from, to} = yield this.args.render(context)
const length = to - from + 1
const value = from + Math.floor(length * Math.random())
emitter.write(value)
}
})
```
`{ max: 10 }` 上下文上渲染 `{% random from:2, to:max %}` 将会得到 [2, 10] 范围内的随机数。见这个 JSFiddle<http://jsfiddle.net/ctj364up/4/>。
[register-tags]: /tutorials/register-filters-tags.html
[Tokenizer]: /api/classes/parser_tokenizer_.tokenizer.html
[Hash]: /api/classes/template_tag_hash_.hash.html
@@ -0,0 +1,89 @@
---
title: 同步和异步
---
LiquidJS 支持同步调用也支持异步调用,支持 Promise。为了同异步复用一套标签和过滤器,LiquidJS 标签用生成器来实现。
## 同异步 API
[Liquid][Liquid] 上主要的方法都支持同步和异步,下面这些方法返回 `Promise`
- `render()`
- `renderFile()`
- `parseFile()`
- `parseAndRender()`
- `evalValue()`
它们的同步版本带一个 `Sync` 后缀:
- `renderSync()`
- `renderFileSync()`
- `parseFileSync()`
- `parseAndRenderSync()`
- `evalValueSync()`
## 如何实现兼容同步的标签
### 要求
所有内置标签都兼容同步,可以安全地用于同步或异步 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
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args
},
* render: function(ctx: Context) {
// 同步调用时 `ctx.sync == true``_evalValue()` 会同步地执行
var str = yield this.liquid._evalValue(this.str, ctx)
return str.toUpperCase()
}
})
```
见这个 JSFiddle<http://jsfiddle.net/ctj364up/6/>。
## 只支持异步的标签
对于只用于异步 API 的标签,或者只能实现为异步的标签,使用生成器语法和 async 语法并没有区别。
例如,如果上面的 `this.liquid._evalValue()` 不会检查 `ctx.sync` 而且总是返回一个 `Promise`,那么即使这个标签用 `* render()``yield this.liquid._evalValue()` 实现,最终也会渲染成 `<object Promise>`
这时可以直接使用 async 语法。注意有些 LiquidJS API 会返回 `Promise`,有些会返回生成器。你需要用 [toPromise][toPromise] API 来把生成器转换为 `Promise`,比如:
```typescript
import { TagToken, Context, Emitter, TopLevelToken, toPromise } from 'liquidjs'
// Usage: {% upper "alice" %}
// Output: ALICE
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args; // name
},
render: async function(ctx: Context) {
var str = await toPromise(this.liquid._evalValue(this.str, ctx));
// Or use the alternate API that returns a Promise
// var str = await this.liquid.evalValue(this.str, ctx);
return str.toUpperCase()
}
});
```
见这个 JSFiddle<http://jsfiddle.net/ctj364up/5/>。
[Liquid]: /api/classes/liquid_.liquid.html
[toPromise]: /api/modules/liquid_.html#toPromise
+3
View File
@@ -41,6 +41,9 @@ sidebar:
advanced: Advanced
caching: Caching
registeration: Register Filters/Tags
access_scope_in_filters: Access Scope in Filters
parse_parameters: Parse Parameters
sync_and_async: Sync and Async
whitespace: Whitespace Control
plugins: Plugins
operators: Operators
+3
View File
@@ -41,6 +41,9 @@ sidebar:
advanced: 高级主题
caching: 缓存
registeration: 注册标签/过滤器
access_scope_in_filters: 过滤器里访问上下文
parse_parameters: 参数解析
sync_and_async: 同步和异步
whitespace: 换行和缩进
plugins: 插件
operators: 运算符