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