mirror of
https://github.com/harttle/liquidjs.git
synced 2026-10-03 09:05:14 -07:00
docs: tutorial for render tag content
This commit is contained in:
+5
-1
@@ -19,8 +19,12 @@ pretty_urls:
|
|||||||
theme: navy
|
theme: navy
|
||||||
|
|
||||||
highlight:
|
highlight:
|
||||||
|
enable: false
|
||||||
|
prismjs:
|
||||||
enable: true
|
enable: true
|
||||||
line_number: false
|
preprocess: true
|
||||||
|
line_number: true
|
||||||
|
tab_replace: ''
|
||||||
|
|
||||||
algolia:
|
algolia:
|
||||||
applicationID: 0X19J927JZ
|
applicationID: 0X19J927JZ
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ tutorials:
|
|||||||
registeration: register-filters-tags.html
|
registeration: register-filters-tags.html
|
||||||
access_scope_in_filters: access-scope-in-filters.html
|
access_scope_in_filters: access-scope-in-filters.html
|
||||||
parse_parameters: parse-parameters.html
|
parse_parameters: parse-parameters.html
|
||||||
|
render_tag_content: render-tag-content.html
|
||||||
sync_and_async: sync-and-async.html
|
sync_and_async: sync-and-async.html
|
||||||
whitespace: whitespace-control.html
|
whitespace: whitespace-control.html
|
||||||
plugins: plugins.html
|
plugins: plugins.html
|
||||||
|
|||||||
@@ -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 `<div class="wrapper"></div>` element:
|
||||||
|
|
||||||
|
```liquid
|
||||||
|
{% wrap %}
|
||||||
|
{{ "hello world!" | capitalize }}
|
||||||
|
{% endwrap %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class='wrapper'>
|
||||||
|
Hello world!
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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("<div class='wrapper'>")
|
||||||
|
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
|
||||||
|
emitter.write("</div>")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`.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: <http://jsfiddle.net/por0zcn1/3/>
|
||||||
|
|
||||||
|
## 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: <http://jsfiddle.net/por0zcn1/4/>. 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 %}
|
||||||
|
<em>Context</em> 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: <http://jsfiddle.net/por0zcn1/2/>
|
||||||
|
|
||||||
|
{% 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
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
title: 渲染标签内容
|
||||||
|
---
|
||||||
|
|
||||||
|
自定义标签可以有内容,也可以嵌套使用。本文描述了如何实现一个由*开始标签*,*结束标签*和之间的*标签内容*的自定义标签。
|
||||||
|
|
||||||
|
## 渲染标签内容
|
||||||
|
|
||||||
|
我们先实现一个简单的 `wrap` 标签,它会把内容包装在 `<div class="wrapper"></div>` 元素里:
|
||||||
|
|
||||||
|
```liquid
|
||||||
|
{% wrap %}
|
||||||
|
{{ "hello world!" | capitalize }}
|
||||||
|
{% endwrap %}
|
||||||
|
```
|
||||||
|
|
||||||
|
期望输出:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class='wrapper'>
|
||||||
|
Hello world!
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
首先 [注册][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("<div class='wrapper'>")
|
||||||
|
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
|
||||||
|
emitter.write("</div>")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`.renderTemplates()` 可能是异步的,因此需要 `yield` 来等它完成。更多关于 LiquidJS 异步的信息可以参考 [同步和异步][async]。`render()` 的其他部分比较直观,这是 JSFiddle 版本:<http://jsfiddle.net/por0zcn1/3/>。
|
||||||
|
|
||||||
|
## 使用 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 链接:<http://jsfiddle.net/por0zcn1/4/>。简单起见,下面的例子都借助 `ParseStream` 来实现。
|
||||||
|
|
||||||
|
## 操作上下文
|
||||||
|
|
||||||
|
上面的 `wrap` 标签看起来没什么用,反正没它也可以很容易地渲染那部分内容。我们现在来实现一个 `repeat` 标签,把内容渲染两次(还可以[加个参数][parameter]让它渲染任意次):
|
||||||
|
|
||||||
|
```liquid
|
||||||
|
{% repeat %}
|
||||||
|
{{ repeat.i }}. {{ "hello world!" | capitalize }}
|
||||||
|
{% endrepeat %}`
|
||||||
|
```
|
||||||
|
|
||||||
|
期望输出:
|
||||||
|
|
||||||
|
```html
|
||||||
|
1. Hello world!
|
||||||
|
2. Hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
你可能注意到了在 `repeat` 上下文里有个额外的变量 `repeat.i`,这就需要我们操作 *上下文*。
|
||||||
|
|
||||||
|
{% note info 上下文 %}
|
||||||
|
<em>上下文</em> 定义了 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 链接:<http://jsfiddle.net/por0zcn1/2/>。
|
||||||
|
|
||||||
|
{% 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
|
||||||
Vendored
+1
@@ -43,6 +43,7 @@ sidebar:
|
|||||||
registeration: Register Filters/Tags
|
registeration: Register Filters/Tags
|
||||||
access_scope_in_filters: Access Scope in Filters
|
access_scope_in_filters: Access Scope in Filters
|
||||||
parse_parameters: Parse Parameters
|
parse_parameters: Parse Parameters
|
||||||
|
render_tag_content: Render Tag Content
|
||||||
sync_and_async: Sync and Async
|
sync_and_async: Sync and Async
|
||||||
whitespace: Whitespace Control
|
whitespace: Whitespace Control
|
||||||
plugins: Plugins
|
plugins: Plugins
|
||||||
|
|||||||
Vendored
+1
@@ -43,6 +43,7 @@ sidebar:
|
|||||||
registeration: 注册标签/过滤器
|
registeration: 注册标签/过滤器
|
||||||
access_scope_in_filters: 过滤器里访问上下文
|
access_scope_in_filters: 过滤器里访问上下文
|
||||||
parse_parameters: 参数解析
|
parse_parameters: 参数解析
|
||||||
|
render_tag_content: 渲染标签内容
|
||||||
sync_and_async: 同步和异步
|
sync_and_async: 同步和异步
|
||||||
whitespace: 换行和缩进
|
whitespace: 换行和缩进
|
||||||
plugins: 插件
|
plugins: 插件
|
||||||
|
|||||||
+14
-30
@@ -1,40 +1,24 @@
|
|||||||
// https://github.com/chriskempson/tomorrow-theme
|
// https://github.com/chriskempson/tomorrow-theme
|
||||||
|
// Tomorrow Night Eighties
|
||||||
:root {
|
:root {
|
||||||
--highlight-background: #eee
|
--highlight-background: #2d2d2d
|
||||||
--highlight-current-line: #efefef
|
--highlight-current-line: #393939
|
||||||
--highlight-selection: #d6d6d6
|
--highlight-selection: #515151
|
||||||
--highlight-foreground: #4d4d4c
|
--highlight-foreground: #cccccc
|
||||||
--highlight-comment: #8e908c
|
--highlight-comment: #999999
|
||||||
--highlight-red: #c82829
|
--highlight-red: #f2777a
|
||||||
--highlight-orange: #f5871f
|
--highlight-orange: #f99157
|
||||||
--highlight-yellow: #eab700
|
--highlight-yellow: #ffcc66
|
||||||
--highlight-green: #718c00
|
--highlight-green: #99cc99
|
||||||
--highlight-aqua: #3e999f
|
--highlight-aqua: #66cccc
|
||||||
--highlight-blue: #4271ae
|
--highlight-blue: #6699cc
|
||||||
--highlight-purple: #8959a8
|
--highlight-purple: #cc99cc
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre, code
|
pre, code
|
||||||
font-family: font-mono
|
font-family: font-mono
|
||||||
color: var(--highlight-foreground)
|
color: var(--highlight-foreground)
|
||||||
background: var(--color-content-bg-hl)
|
background: var(--highlight-background)
|
||||||
|
|
||||||
code
|
code
|
||||||
padding: 0 5px
|
padding: 0 5px
|
||||||
|
|||||||
-2
@@ -10,7 +10,6 @@ vendor-prefixes = webkit moz ms official
|
|||||||
--color-border: #e3e3e3
|
--color-border: #e3e3e3
|
||||||
--color-navy: hsl(210, 25%, 12%)
|
--color-navy: hsl(210, 25%, 12%)
|
||||||
--color-content-bg: #fff
|
--color-content-bg: #fff
|
||||||
--color-content-bg-hl: #eee
|
|
||||||
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
|
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
|
||||||
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
|
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
|
||||||
--color-link: #0e83cd
|
--color-link: #0e83cd
|
||||||
@@ -26,7 +25,6 @@ vendor-prefixes = webkit moz ms official
|
|||||||
--color-backgrond: #fff
|
--color-backgrond: #fff
|
||||||
--color-navy: hsl(210, 25%, 12%)
|
--color-navy: hsl(210, 25%, 12%)
|
||||||
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
|
--color-news-bg: darken(hsl(210, 25%, 12%), 5%)
|
||||||
--color-content-bg-hl: #000
|
|
||||||
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
|
--color-navy-lighter: lighten(hsl(210, 25%, 12%), 10%)
|
||||||
--color-content-bg: #1c1c1c
|
--color-content-bg: #1c1c1c
|
||||||
--color-link: #0e83cd
|
--color-link: #0e83cd
|
||||||
|
|||||||
Reference in New Issue
Block a user