From 394d5a1ae2370f1083c65deb2c555e98205a73fc Mon Sep 17 00:00:00 2001 From: Harttle Date: Sat, 22 Jan 2022 18:52:37 +0800 Subject: [PATCH] docs: add tutorials for custom filters and tags --- docs/source/_data/sidebar.yml | 3 + .../tutorials/access-scope-in-filters.md | 32 ++++++ docs/source/tutorials/parse-parameters.md | 100 ++++++++++++++++++ .../source/tutorials/register-filters-tags.md | 2 +- docs/source/tutorials/sync-and-async.md | 90 ++++++++++++++++ .../tutorials/access-scope-in-filters.md | 32 ++++++ .../zh-cn/tutorials/parse-parameters.md | 100 ++++++++++++++++++ docs/source/zh-cn/tutorials/sync-and-async.md | 89 ++++++++++++++++ docs/themes/navy/languages/en.yml | 3 + docs/themes/navy/languages/zh-cn.yml | 3 + 10 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 docs/source/tutorials/access-scope-in-filters.md create mode 100644 docs/source/tutorials/parse-parameters.md create mode 100644 docs/source/tutorials/sync-and-async.md create mode 100644 docs/source/zh-cn/tutorials/access-scope-in-filters.md create mode 100644 docs/source/zh-cn/tutorials/parse-parameters.md create mode 100644 docs/source/zh-cn/tutorials/sync-and-async.md diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index 53e25330b..1b6d0717e 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -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 diff --git a/docs/source/tutorials/access-scope-in-filters.md b/docs/source/tutorials/access-scope-in-filters.md new file mode 100644 index 000000000..fa95c5491 --- /dev/null +++ b/docs/source/tutorials/access-scope-in-filters.md @@ -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: + +{% note warn Arrow Functions %} +this in arrow functions is bound to current JavaScript context, you'll need to use function(){} instead of ()=>{} syntax to access this.context correctly. +{% endnote %} + +[register-filters]: /tutorials/register-filters-tags.html diff --git a/docs/source/tutorials/parse-parameters.md b/docs/source/tutorials/parse-parameters.md new file mode 100644 index 000000000..f347d2fe2 --- /dev/null +++ b/docs/source/tutorials/parse-parameters.md @@ -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: + +## 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: + +{% 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 Sync and Async 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: + + +[register-tags]: /tutorials/register-filters-tags.html +[Tokenizer]: /api/classes/parser_tokenizer_.tokenizer.html +[Hash]: /api/classes/template_tag_hash_.hash.html diff --git a/docs/source/tutorials/register-filters-tags.md b/docs/source/tutorials/register-filters-tags.md index 7d1e21151..961ca1ada 100644 --- a/docs/source/tutorials/register-filters-tags.md +++ b/docs/source/tutorials/register-filters-tags.md @@ -61,4 +61,4 @@ function disabledFilter(name) { } } engine.registerFilter('plus', disabledFilter('plus')); -``` +``` \ No newline at end of file diff --git a/docs/source/tutorials/sync-and-async.md b/docs/source/tutorials/sync-and-async.md new file mode 100644 index 000000000..1929dd9a9 --- /dev/null +++ b/docs/source/tutorials/sync-and-async.md @@ -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 `, and +- Should not be declared as `async`. + +{% note info Non Sync-Compatible Tags %} +Non sync-compatible tags are also valid tags, will work just fine for asynchronous API calls. When called synchronously, tags that return a Promise will be rendered as [object Promise]. +{% 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: . + +## 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 `` 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: . + +[Liquid]: /api/classes/liquid_.liquid.html +[toPromise]: /api/modules/liquid_.html#toPromise diff --git a/docs/source/zh-cn/tutorials/access-scope-in-filters.md b/docs/source/zh-cn/tutorials/access-scope-in-filters.md new file mode 100644 index 000000000..e93d0f184 --- /dev/null +++ b/docs/source/zh-cn/tutorials/access-scope-in-filters.md @@ -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:。 + +{% note warn 箭头函数 %} +在箭头函数里 `this` 会绑定到当前 JavaScript 上下文,你需要用 `function(){}` 来替代 `()=>{}` 语法,才能正确地访问 `this.context`。 +{% endnote %} + +[register-filters]: /tutorials/register-filters-tags.html diff --git a/docs/source/zh-cn/tutorials/parse-parameters.md b/docs/source/zh-cn/tutorials/parse-parameters.md new file mode 100644 index 000000000..df05d4b31 --- /dev/null +++ b/docs/source/zh-cn/tutorials/parse-parameters.md @@ -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:。 + +## 解析参数的值 + +除了静态的参数字符串之外,我们更希望把动态的值传递给标签。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:. + +{% note info 异步和 Promise %} +在 LiquidJS 里异步用生成器实现,这样同样一份标签的实现也可以用于同步的 API 比如 `renderSync()`,`parseAndRenderSync()`,`renderFileSync()`。如果要在标签实现里等待 Promise,只需要把 `await somePromise` 换成 `yield somePromise`,并保留 `* render()` 不要改成 `async render()`。更多细节请参考 Sync and Async。 +{% 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:。 + +[register-tags]: /tutorials/register-filters-tags.html +[Tokenizer]: /api/classes/parser_tokenizer_.tokenizer.html +[Hash]: /api/classes/template_tag_hash_.hash.html diff --git a/docs/source/zh-cn/tutorials/sync-and-async.md b/docs/source/zh-cn/tutorials/sync-and-async.md new file mode 100644 index 000000000..b90f2917b --- /dev/null +++ b/docs/source/zh-cn/tutorials/sync-and-async.md @@ -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 `, +- 也不能声明为 `async`。 + +{% note info 不兼容同步的标签 %} +不兼容同步的标签也仍然是合法标签,在异步 API 下也会正常运行。被同步调用时,返回 Promise 的标签会被渲染成 [object Promise]。 +{% 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:。 + +## 只支持异步的标签 + +对于只用于异步 API 的标签,或者只能实现为异步的标签,使用生成器语法和 async 语法并没有区别。 + +例如,如果上面的 `this.liquid._evalValue()` 不会检查 `ctx.sync` 而且总是返回一个 `Promise`,那么即使这个标签用 `* render()` 和 `yield this.liquid._evalValue()` 实现,最终也会渲染成 ``。 + +这时可以直接使用 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:。 + +[Liquid]: /api/classes/liquid_.liquid.html +[toPromise]: /api/modules/liquid_.html#toPromise diff --git a/docs/themes/navy/languages/en.yml b/docs/themes/navy/languages/en.yml index dcc80780c..70199d266 100644 --- a/docs/themes/navy/languages/en.yml +++ b/docs/themes/navy/languages/en.yml @@ -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 diff --git a/docs/themes/navy/languages/zh-cn.yml b/docs/themes/navy/languages/zh-cn.yml index e2f773c1e..5b713ce0d 100644 --- a/docs/themes/navy/languages/zh-cn.yml +++ b/docs/themes/navy/languages/zh-cn.yml @@ -41,6 +41,9 @@ sidebar: advanced: 高级主题 caching: 缓存 registeration: 注册标签/过滤器 + access_scope_in_filters: 过滤器里访问上下文 + parse_parameters: 参数解析 + sync_and_async: 同步和异步 whitespace: 换行和缩进 plugins: 插件 operators: 运算符