diff --git a/docs/_config.yml b/docs/_config.yml
index 56f2b00cb..63d398297 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -19,8 +19,12 @@ pretty_urls:
theme: navy
highlight:
+ enable: false
+prismjs:
enable: true
- line_number: false
+ preprocess: true
+ line_number: true
+ tab_replace: ''
algolia:
applicationID: 0X19J927JZ
diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml
index 1b6d0717e..71ceab45c 100644
--- a/docs/source/_data/sidebar.yml
+++ b/docs/source/_data/sidebar.yml
@@ -11,6 +11,7 @@ tutorials:
registeration: register-filters-tags.html
access_scope_in_filters: access-scope-in-filters.html
parse_parameters: parse-parameters.html
+ render_tag_content: render-tag-content.html
sync_and_async: sync-and-async.html
whitespace: whitespace-control.html
plugins: plugins.html
diff --git a/docs/source/tutorials/render-tag-content.md b/docs/source/tutorials/render-tag-content.md
new file mode 100644
index 000000000..edbc4c324
--- /dev/null
+++ b/docs/source/tutorials/render-tag-content.md
@@ -0,0 +1,137 @@
+---
+title: Render Tag Content
+---
+
+Custom tags can have content template and can be nested. This article describes how to implement custom tags that consists of a *begin tag*, an *end tag*, and template content between them.
+
+## Render Tag Content
+
+We'll start with a simple tag `wrap` which wraps its content into a `
` element:
+
+```liquid
+{% wrap %}
+ {{ "hello world!" | capitalize }}
+{% endwrap %}
+```
+
+Expected output:
+
+```html
+
+ Hello world!
+
+```
+
+Firstly, [register][register-tags] a tag with name `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`,
+
+- `tagToken` is current token `{% wrap %}`, and
+- `remainTokens` is an array of all tokens following `{% wrap %}` until the end of this template file.
+
+Basically, what we need to do is take/`.shift()` enough tags from `remainTokens` until we got a `endwrap` token (the name can be arbitrary, but in convention, we need it to be `endwrap`). And if there's no `endwrap` until the end of template file, we need to throw an tag-not-closed `Error`.
+
+```javascript
+engine.registerTag('wrap', {
+ parse(tagToken, remainTokens) {
+ this.tpls = []
+ let closed = false
+ while(remainTokens.length) {
+ let token = remainTokens.shift()
+ // we got the end tag! stop taking tokens
+ if (token.name === 'endwrap') {
+ closed = true
+ break
+ }
+ // parse token into template
+ // parseToken() may consume more than 1 tokens
+ // e.g. {% if %}...{% endif %}
+ let tpl = this.liquid.parser.parseToken(token, remainTokens)
+ this.tpls.push(tpl)
+ }
+ if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
+ },
+ * render(context, emitter) {
+ emitter.write("")
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ emitter.write("
")
+ }
+})
+```
+
+`.renderTemplates()` can be async, we need `yield` to wait it complete. More details on async in LiquidJS, please refer to [Sync and Async][async]. Other parts of `render()` method is quite straightforward. Here's a JSFiddle version:
+
+## Using ParseStream
+
+When it comes to complex tags like [for][for] and [if][if], the `parse()` can be very complicated. There's a [ParseStream][ParseStream] utility to organize the `parse()` in event-based style. Following is a re-written `parse()` using `ParseStream` and does exactly the same as above example.
+
+```javascript
+parse(tagToken, remainTokens) {
+ this.tpls = []
+ this.liquid.parser.parseStream(remainTokens)
+ .on('template', tpl => this.tpls.push(tpl))
+ // note that we cannot use arrow function because we need `this`
+ .on('tag:endwrap', function () { this.stop() })
+ .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
+ .start()
+}
+```
+
+Here's a JSFiddle version: . For simplicity, the following examples are implemented using `ParseStream`.
+
+## Manipulate the Context
+
+The `wrap` tag above doesn't seem to be very useful, without using that tag we can render the content anyway. Now we're going to implement a `repeat` tag to render the content 2 times (we can also add a [parameter][parameter] to render arbitrary times).
+
+```liquid
+{% repeat %}
+ {{ repeat.i }}. {{ "hello world!" | capitalize }}
+{% endrepeat %}`
+```
+
+Expected outputs:
+
+```html
+1. Hello world!
+2. Hello world!
+```
+
+As you've noticed, there's an additional `repeat.i` in the context of `repeat`. That is implemented by manipulating the *Context*.
+
+{% note info Context %}
+Context defines the value of each variable in Liquid template. In LiquidJS, a `Context` consists of a stack of `Scope`s. A *Scope* is a plain object like the one specified in `engine.render(tpl, scope)`.
+{% endnote %}
+
+Each time we enter a new *Context*, we need to push a new *Scope*. And when we finish rendering and exit the *Context*, we pop the *Scope* from the *Context*. As you can see in the following implementation:
+
+```javascript
+engine.registerTag('repeat', {
+ parse(tagToken, remainTokens) {
+ this.tpls = []
+ this.liquid.parser.parseStream(remainTokens)
+ .on('template', tpl => this.tpls.push(tpl))
+ .on('tag:endrepeat', function () { this.stop() })
+ .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
+ .start()
+ },
+ * render(context, emitter) {
+ const repeat = { i: 1 }
+ context.push({ repeat })
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ repeat.i++
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ context.pop()
+ }
+})
+```
+
+The `parse()` is exactly the same as `wrap` tag, we repeat the content simply by calling `.renderTemplates(this.tpls)` twice during `render()`. Here's the JSFiddle:
+
+{% note warn Use Push & Pop in Pairs %}
+`context.push()` and `context.pop()` have to be used in pairs. Failing to `pop()` the *Scope* you pushed will leak the *Scope* to latter templates and may corrupt the *Context* stack.
+{% endnote %}
+
+[register-tags]: ./register-filters-tags.html
+[async]: ./sync-and-async.html
+[for]: ../tags/for.html
+[if]: ../tags/if.html
+[ParseStream]: ../api/classes/parser_parse_stream_.parsestream.html
+[parameter]: ./parse-parameters.html
diff --git a/docs/source/zh-cn/tutorials/render-tag-content.md b/docs/source/zh-cn/tutorials/render-tag-content.md
new file mode 100644
index 000000000..06a71f1e6
--- /dev/null
+++ b/docs/source/zh-cn/tutorials/render-tag-content.md
@@ -0,0 +1,137 @@
+---
+title: 渲染标签内容
+---
+
+自定义标签可以有内容,也可以嵌套使用。本文描述了如何实现一个由*开始标签*,*结束标签*和之间的*标签内容*的自定义标签。
+
+## 渲染标签内容
+
+我们先实现一个简单的 `wrap` 标签,它会把内容包装在 `` 元素里:
+
+```liquid
+{% wrap %}
+ {{ "hello world!" | capitalize }}
+{% endwrap %}
+```
+
+期望输出:
+
+```html
+
+ Hello world!
+
+```
+
+首先 [注册][register-tags] 一个名为 `wrap` 的标签,把内容解析到 `this.tpls` 数组里。`parse(tagToken, remainTokens)` 中,
+
+- `tagToken` 是当前 *Token* `{% wrap %}`,
+- `remainTokens` 是当前模板中后续所有 *Token* 的数组。
+
+我们要做的是从 `remainTokens` 里拿出来/`.shift()` 足够的标签直到遇到 `endwrap`(其实可以是任意名字,但按照惯例应该叫 `endwrap`)。如果到模板结尾都没遇到 `endwrap`,需要抛出一个标签未关闭的 `Error`。
+
+```javascript
+engine.registerTag('wrap', {
+ parse(tagToken, remainTokens) {
+ this.tpls = []
+ let closed = false
+ while(remainTokens.length) {
+ let token = remainTokens.shift()
+ // 得到了结束标签,停止解析
+ if (token.name === 'endwrap') {
+ closed = true
+ break
+ }
+ // 把 Token 解析成 Template
+ // parseToken() 可能会消耗多个 Token
+ // 例如 {% if %}...{% endif %}
+ let tpl = this.liquid.parser.parseToken(token, remainTokens)
+ this.tpls.push(tpl)
+ }
+ if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
+ },
+ * render(context, emitter) {
+ emitter.write("")
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ emitter.write("
")
+ }
+})
+```
+
+`.renderTemplates()` 可能是异步的,因此需要 `yield` 来等它完成。更多关于 LiquidJS 异步的信息可以参考 [同步和异步][async]。`render()` 的其他部分比较直观,这是 JSFiddle 版本:。
+
+## 使用 ParseStream
+
+对于像 [for][for] 和 [if][if] 这样的复杂标签,`parse()` 会变得很复杂。使用 [ParseStream][ParseStream] 工具可以按事件风格来组织 `parse()` 的逻辑。下面是用 `ParseStream` 重写过的 `parse()`,实现了和上面例子中完全一样的功能。
+
+```javascript
+parse(tagToken, remainTokens) {
+ this.tpls = []
+ this.liquid.parser.parseStream(remainTokens)
+ .on('template', tpl => this.tpls.push(tpl))
+ // 注意这里不能用箭头函数,因为我们需要 `this`
+ .on('tag:endwrap', function () { this.stop() })
+ .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
+ .start()
+}
+```
+
+这是 JSFiddle 链接:。简单起见,下面的例子都借助 `ParseStream` 来实现。
+
+## 操作上下文
+
+上面的 `wrap` 标签看起来没什么用,反正没它也可以很容易地渲染那部分内容。我们现在来实现一个 `repeat` 标签,把内容渲染两次(还可以[加个参数][parameter]让它渲染任意次):
+
+```liquid
+{% repeat %}
+ {{ repeat.i }}. {{ "hello world!" | capitalize }}
+{% endrepeat %}`
+```
+
+期望输出:
+
+```html
+1. Hello world!
+2. Hello world!
+```
+
+你可能注意到了在 `repeat` 上下文里有个额外的变量 `repeat.i`,这就需要我们操作 *上下文*。
+
+{% note info 上下文 %}
+上下文 定义了 Liquid 模板中每个变量的值。在 LiquidJS 中,`Context` 由一个 `Scope` 的栈组成。*Scope* 就是一个普通对象,就像传给 `engine.render(tpl, scope)` 的 `scope` 一样。
+{% endnote %}
+
+每次进入新的 *上下文* 时,我们需要 `push` 一个新的 `Scope`。当结束渲染并退出 *上下文* 时,再把 `Scope` 从 *上下文* `pop` 出来。见下面的实现:
+
+```javascript
+engine.registerTag('repeat', {
+ parse(tagToken, remainTokens) {
+ this.tpls = []
+ this.liquid.parser.parseStream(remainTokens)
+ .on('template', tpl => this.tpls.push(tpl))
+ .on('tag:endrepeat', function () { this.stop() })
+ .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
+ .start()
+ },
+ * render(context, emitter) {
+ const repeat = { i: 1 }
+ context.push({ repeat })
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ repeat.i++
+ yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
+ context.pop()
+ }
+})
+```
+
+`parse()` 部分和 `wrap` 标签完全相同,在 `render()` 部分我们通过调用两次 `.renderTemplates(this.tpls)` 来重复渲染内容。这是 JSFiddle 链接:。
+
+{% note warn 成对使用 Push 和 Pop %}
+必须成对地使用 `context.push()` 和 `context.pop()`。如果忘记 `pop()` 会导致 `Scope` 泄露给后面的模板内容,也可能损坏 *上下文* 栈。.
+{% endnote %}
+
+[register-tags]: ./register-filters-tags.html
+[async]: ./sync-and-async.html
+[for]: ../tags/for.html
+[if]: ../tags/if.html
+[ParseStream]: ../api/classes/parser_parse_stream_.parsestream.html
+[parameter]: ./parse-parameters.html
diff --git a/docs/themes/navy/languages/en.yml b/docs/themes/navy/languages/en.yml
index 70199d266..ab3562721 100644
--- a/docs/themes/navy/languages/en.yml
+++ b/docs/themes/navy/languages/en.yml
@@ -43,6 +43,7 @@ sidebar:
registeration: Register Filters/Tags
access_scope_in_filters: Access Scope in Filters
parse_parameters: Parse Parameters
+ render_tag_content: Render Tag Content
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 5b713ce0d..7b7caaa27 100644
--- a/docs/themes/navy/languages/zh-cn.yml
+++ b/docs/themes/navy/languages/zh-cn.yml
@@ -43,6 +43,7 @@ sidebar:
registeration: 注册标签/过滤器
access_scope_in_filters: 过滤器里访问上下文
parse_parameters: 参数解析
+ render_tag_content: 渲染标签内容
sync_and_async: 同步和异步
whitespace: 换行和缩进
plugins: 插件
diff --git a/docs/themes/navy/source/css/_partial/highlight.styl b/docs/themes/navy/source/css/_partial/highlight.styl
index 8e83e65f4..5fba995ec 100644
--- a/docs/themes/navy/source/css/_partial/highlight.styl
+++ b/docs/themes/navy/source/css/_partial/highlight.styl
@@ -1,40 +1,24 @@
// https://github.com/chriskempson/tomorrow-theme
+// Tomorrow Night Eighties
:root {
- --highlight-background: #eee
- --highlight-current-line: #efefef
- --highlight-selection: #d6d6d6
- --highlight-foreground: #4d4d4c
- --highlight-comment: #8e908c
- --highlight-red: #c82829
- --highlight-orange: #f5871f
- --highlight-yellow: #eab700
- --highlight-green: #718c00
- --highlight-aqua: #3e999f
- --highlight-blue: #4271ae
- --highlight-purple: #8959a8
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --highlight-background: #000000
- --highlight-current-line: #2a2a2a
- --highlight-selection: #424242
- --highlight-foreground: #eaeaea
- --highlight-comment: #969896
- --highlight-red: #d54e53
- --highlight-orange: #e78c45
- --highlight-yellow: #e7c547
- --highlight-green: #b9ca4a
- --highlight-aqua: #70c0b1
- --highlight-blue: #7aa6da
- --highlight-purple: #c397d8
- }
+ --highlight-background: #2d2d2d
+ --highlight-current-line: #393939
+ --highlight-selection: #515151
+ --highlight-foreground: #cccccc
+ --highlight-comment: #999999
+ --highlight-red: #f2777a
+ --highlight-orange: #f99157
+ --highlight-yellow: #ffcc66
+ --highlight-green: #99cc99
+ --highlight-aqua: #66cccc
+ --highlight-blue: #6699cc
+ --highlight-purple: #cc99cc
}
pre, code
font-family: font-mono
color: var(--highlight-foreground)
- background: var(--color-content-bg-hl)
+ background: var(--highlight-background)
code
padding: 0 5px
diff --git a/docs/themes/navy/source/css/_variables.styl b/docs/themes/navy/source/css/_variables.styl
index 999799007..199128b55 100644
--- a/docs/themes/navy/source/css/_variables.styl
+++ b/docs/themes/navy/source/css/_variables.styl
@@ -10,7 +10,6 @@ vendor-prefixes = webkit moz ms official
--color-border: #e3e3e3
--color-navy: hsl(210, 25%, 12%)
--color-content-bg: #fff
- --color-content-bg-hl: #eee
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
--color-link: #0e83cd
@@ -26,7 +25,6 @@ vendor-prefixes = webkit moz ms official
--color-backgrond: #fff
--color-navy: hsl(210, 25%, 12%)
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
- --color-content-bg-hl: #000
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
--color-content-bg: #1c1c1c
--color-link: #0e83cd