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 %}
+
valueOf()
is typically used to define how the current variable should be rendered, while toLiquid()
is often used to convert an object into a Drop or another scope provided to the template.valueOf()
is a method exclusive to Drops; whereas toLiquid()
can be used on any scope object.valueOf()
is called when the variable itself is about to be rendered, replacing itself; whereas toLiquid()
is called when its properties are about to be read.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()
的区别 %}
+valueOf()
通常用来定义当前变量如何渲染,toLiquid()
通常用来把一个对象转换为 Drop 或另一个提供给模板的 scope。valueOf()
是 Drop 才有的方法;而 toLiquid()
可以用在任何 scope 对象上。valueOf()
是在自己即将被渲染时,用来替代自己;而 toLiquid()
在即将读取它的属性时才会被调用。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%)