diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index 99312b03c9..6c5ba97b16 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -12,6 +12,7 @@ tutorials: access_scope_in_filters: access-scope-in-filters.html parse_parameters: parse-parameters.html render_tag_content: render-tag-content.html + drops: drops.html sync_and_async: sync-and-async.html whitespace: whitespace-control.html plugins: plugins.html diff --git a/docs/source/tutorials/drops.md b/docs/source/tutorials/drops.md new file mode 100644 index 0000000000..5915525ce7 --- /dev/null +++ b/docs/source/tutorials/drops.md @@ -0,0 +1,201 @@ +--- +title: Liquid Drops +--- + +LiquidJS also provides a mechanism similar to [Shopify Drops][shopify-drops], allowing template authors to incorporate custom functionality in resolving variable values. + +{% note info Drop for JavaScript %} +Drop interface is implemented differently in LiquidJS compared to built-in filters and other template functionalities. Since LiquidJS runs in JavaScript, custom Drops need to be reimplemented in JavaScript anyway. There's no compatibility between JavaScript classes and Ruby classes. +{% endnote %} + +## Basic Usage + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class SettingsDrop extends Drop { + constructor() { + super() + this.foo = 'FOO' + } + bar() { + return 'BAR' + } +} + +const engine = new Liquid() +const template = `foo: {{settings.foo}}, bar: {{settings.bar}}` +const context = { settings: new SettingsDrop() } +// Outputs: "foo: FOO, bar: BAR" +engine.parseAndRender(template, context).then(html => console.log(html)) +``` + +[Runkit link](https://runkit.com/embed/2is7di4mc7kk) + +As shown above, besides reading properties from context scopes, you can also call methods. You only need to create a custom class inherited from `Drop`. + +{% note tip Async Methods %} +LiquidJS is fully async-friendly. You can safely return a Promise in your Drop methods or define your methods in Drop as `async`. +{% endnote %} + +## liquidMethodMissing + +For cases when there isn't a fixed set of properties, you can leverage `liquidMethodMissing` to dynamically resolve the value of a variable name. + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class SettingsDrop extends Drop { + liquidMethodMissing(key) { + return key.toUpperCase() + } +} + +const engine = new Liquid() +// Outputs: "COO" +engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() }) + .then(html => console.log(html)) +``` + +`liquidMethodMissing` supports Promise, meaning you can make async calls within it. A more useful case can be fetching the value dynamically from the database. By using Drops, you can avoid hardcoding each property into the context. For example: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class DBDrop extends Drop { + async liquidMethodMissing(key) { + const record = await db.getRecordByKey(key) + return record.value + } +} + +const engine = new Liquid() +const context = { db: new DBDrop() } +engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html)) +``` + +## valueOf + +Drops can implement a `valueOf()` method, the return value of which can be used to replace itself in the output. For example: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class ColorDrop extends Drop { + valueOf() { + return 'red' + } +} + +const engine = new Liquid() +const context = { color: new ColorDrop() } +// Outputs: "red" +engine.parseAndRender("{{color}}", context).then(html => console.log(html)) +``` + +## toLiquid + +`toLiquid()` is not a method of `Drop`, but it can be used to return a `Drop`. In cases where you have a fixed structure in the `context` that cannot change its values, you can implement `toLiquid()` to let LiquidJS use the returned value instead of itself to render the templates. + +```javascript +import { Liquid, Drop } from 'liquidjs' + +const context = { + person: { + firstName: "Jun", + lastName: "Yang", + name: "Jun Yang", + toLiquid: () => ({ + firstName: this.firstName, + lastName: this.lastName, + // use a different `name` + name: "Yang, Jun" + }) + } +} + +const engine = new Liquid() +// Outputs: "Yang, Jun" +engine.parseAndRender("{{person.name}}", context).then(html => console.log(html)) +``` + +Of course, you can also return a `PersonDrop` instance in the `toLiquid()` method and implement this functionality within `PersonDrop`: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class PersonDrop extends Drop { + constructor(person) { + super() + this.person = person + } + name() { + return this.person.lastName + ", " + this.person.firstName + } +} + +const context = { + person: { + firstName: "Jun", + lastName: "Yang", + name: "Jun Yang", + toLiquid: function () { return new PersonDrop(this) } + } +} + +const engine = new Liquid() +// Outputs: "Yang, Jun" +engine.parseAndRender("{{person.name}}", context).then(html => console.log(html)) +``` + +{% note info toLiquid() vs. valueOf() Difference %} + +{% endnote %} + +## Special Drops + +LiquidJS itself implements several built-in drops to facilitate template writing. This part is compatible with Shopify Liquid, as we need templates to be portable. + +### blank + +Useful to check whether a string variable is `false`, `null`, `undefined`, an empty string, or a string containing only blank characters. + +```liquid +{% unless author == blank %} + {{author}} +{% endif %} +``` + +### empty + +Useful to check if an array, string, or object is empty. + +```liquid +{% if authors == empty %} + Author list is empty +{% endif %} +``` + +{% note info empty implementation %} +For arrays and strings, LiquidJS checks their `.length` property. For objects, LiquidJS calls `Object.keys()` to check whether they have keys. +{% endnote %} + +### nil + +`nil` Drop is used to check whether a variable is not defined or defined as `null` or `undefined`, essentially equivalent to JavaScript `== null` check. + +```liquid +{% if notexist == nil %} + null variable +{% endif %} +``` + +### Other Drops + +There are still several Drops for specific tags, like `forloop`, `tablerowloop`, `block`, which are covered by respective tag documents. + +[shopify-drops]: https://github.com/Shopify/liquid/wiki/Introduction-to-Drops diff --git a/docs/source/zh-cn/tutorials/drops.md b/docs/source/zh-cn/tutorials/drops.md new file mode 100644 index 0000000000..7f41c8bd98 --- /dev/null +++ b/docs/source/zh-cn/tutorials/drops.md @@ -0,0 +1,201 @@ +--- +title: Liquid Drop +--- + +LiquidJS 还提供了一种类似于 [Shopify Drop][shopify-drops] 的机制,用于为模板作者提供在自定义解析变量值的功能。 + +{% note info JavaScript 中的 Drop %} +Drop 接口在 LiquidJS 中实现方式与内置过滤器和其他模板功能不同。由于 LiquidJS 在 JavaScript 中运行,自定义 Drop 在 JavaScript 中一定需要重新实现。JavaScript 类与 Ruby 类之间没有兼容性可言。 +{% endnote %} + +## 基本用法 + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class SettingsDrop extends Drop { + constructor() { + super() + this.foo = 'FOO' + } + bar() { + return 'BAR' + } +} + +const engine = new Liquid() +const template = `foo: {{settings.foo}}, bar: {{settings.bar}}` +const context = { settings: new SettingsDrop() } +// 输出: "foo: FOO, bar: BAR" +engine.parseAndRender(template, context).then(html => console.log(html)) +``` + +[Runkit 链接](https://runkit.com/embed/2is7di4mc7kk) + +如上所示,除了从上下文作用域中读取属性外,还可以调用方法。您只需创建一个继承自 `Drop` 的自定义类。 + +{% note tip 异步方法 %} +LiquidJS 完全支持异步,您可以在 Drop 的方法中安全地返回 Promise,或将 Drop 的方法定义为 `async`。 +{% endnote %} + +## liquidMethodMissing + +如果属性名不能静态地确定的情况下,可以利用 `liquidMethodMissing` 来动态解析变量的值。 + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class SettingsDrop extends Drop { + liquidMethodMissing(key) { + return key.toUpperCase() + } +} + +const engine = new Liquid() +// 输出: "COO" +engine.parseAndRender("{{settings.coo}}", { settings: new SettingsDrop() }) + .then(html => console.log(html)) +``` + +`liquidMethodMissing` 支持 Promise,这意味着您可以在其中进行异步调用。一个更有用的例子是通过使用 Drop 动态地从数据库获取值。通过使用 Drop,您可以避免将每个属性都硬编码到上下文中。例如: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class DBDrop extends Drop { + async liquidMethodMissing(key) { + const record = await db.getRecordByKey(key) + return record.value + } +} + +const engine = new Liquid() +const context = { db: new DBDrop() } +engine.parseAndRender("{{db.coo}}", context).then(html => console.log(html)) +``` + +## valueOf + +Drop 可以实现一个 `valueOf()` 方法,用于在输出中替换自身。例如: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class ColorDrop extends Drop { + valueOf() { + return 'red' + } +} + +const engine = new Liquid() +const context = { color: new ColorDrop() } +// 输出: "red" +engine.parseAndRender("{{color}}", context).then(html => console.log(html)) +``` + +## toLiquid + +`toLiquid()` 不是 `Drop` 的方法,但它可以用于返回一个 `Drop`。在您有一个上下文中固定结构且不能更改其值的情况下,您可以实现 `toLiquid()`,以便让 LiquidJS 使用返回的值而不是自身来渲染模板。 + +```javascript +import { Liquid, Drop } from 'liquidjs' + +const context = { + person: { + firstName: "Jun", + lastName: "Yang", + name: "Jun Yang", + toLiquid: () => ({ + firstName: this.firstName, + lastName: this.lastName, + // 使用不同的 `name` + name: "Yang, Jun" + }) + } +} + +const engine = new Liquid() +// 输出: "Yang, Jun" +engine.parseAndRender("{{person.name}}", context).then(html => console.log(html)) +``` + +当然,您还可以在 `toLiquid()` 方法中返回一个 `PersonDrop` 实例,并在 `PersonDrop` 中实现此功能: + +```javascript +import { Liquid, Drop } from 'liquidjs' + +class PersonDrop extends Drop { + constructor(person) { + super() + this.person = person + } + name() { + return this.person.lastName + ", " + this.person.firstName + } +} + +const context = { + person: { + firstName: "Jun", + lastName: "Yang", + name: "Jun Yang", + toLiquid: function () { return new PersonDrop(this) } + } +} + +const engine = new Liquid() +// 输出: "Yang, Jun" +engine.parseAndRender("{{person.name}}", context).then(html => console.log(html)) +``` + +{% note info toLiquid()valueOf() 的区别 %} + +{% endnote %} + +## 特殊 Drop + +LiquidJS 本身实现了几个内置 Drop,以促进模板编写。此部分与 Shopify Liquid 兼容,因为我们需要模板具有可移植性。 + +### blank + +用于检查字符串变量是否为 `false`、`null`、`undefined`、空字符串或字符串仅包含空白字符。 + +```liquid +{% unless author == blank %} + {{author}} +{% endif %} +``` + +### empty + +用于检查数组、字符串或对象是否为空。 + +```liquid +{% if authors == empty %} + 作者列表为空 +{% endif %} +``` + +{% note info empty 的实现 %} +对于数组和字符串,LiquidJS 检查它们的 `.length` 属性。对于对象,LiquidJS 调用 `Object.keys()` 来检查它是否有键。 +{% endnote %} + +### nil + +`nil` Drop 用于检查变量是否未定义或定义为 `null` 或 `undefined`,本质上等同于 JavaScript 的 `== null` 检查。 + +```liquid +{% if notexist == nil %} + 空变量 +{% endif %} +``` + +### 其他 Drop + +仍然有一些特定标签的 Drop,例如 `forloop`、`tablerowloop`、`block`,这些在各自的标签文档中有详细介绍。 + +[shopify-drops]: https://github.com/Shopify/liquid/wiki/Introduction-to-Drops diff --git a/docs/themes/navy/languages/en.yml b/docs/themes/navy/languages/en.yml index 8121867917..6bbc7e7d30 100644 --- a/docs/themes/navy/languages/en.yml +++ b/docs/themes/navy/languages/en.yml @@ -47,6 +47,7 @@ sidebar: access_scope_in_filters: Access Scope in Filters parse_parameters: Parse Parameters render_tag_content: Render Tag Content + drops: Liquid Drops sync_and_async: Sync and Async whitespace: Whitespace Control plugins: Plugins diff --git a/docs/themes/navy/languages/zh-cn.yml b/docs/themes/navy/languages/zh-cn.yml index 7b7caaa27c..9333eec192 100644 --- a/docs/themes/navy/languages/zh-cn.yml +++ b/docs/themes/navy/languages/zh-cn.yml @@ -44,6 +44,7 @@ sidebar: access_scope_in_filters: 过滤器里访问上下文 parse_parameters: 参数解析 render_tag_content: 渲染标签内容 + drops: Liquid Drop sync_and_async: 同步和异步 whitespace: 换行和缩进 plugins: 插件 diff --git a/docs/themes/navy/source/css/_partial/page.styl b/docs/themes/navy/source/css/_partial/page.styl index 913124d047..02fa49ef74 100644 --- a/docs/themes/navy/source/css/_partial/page.styl +++ b/docs/themes/navy/source/css/_partial/page.styl @@ -1,4 +1,4 @@ -note-tip = hsl(40, 100%, 50%) +note-tip = #0fff00 note-info = hsl(200, 100%, 50%) note-warn = hsl(0, 100%, 50%)