docs: revert unnecessary tutorial churn from memoryLimit PR

Restore the two-example register-filters-tags structure (Value + Hash)
and undo unrelated constructor/emitter doc edits not required for DoS limits.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-07-14 21:41:46 +08:00
co-authored by Cursor
parent e88bf4aba3
commit d0b61fc5fe
3 changed files with 49 additions and 44 deletions
+34 -20
View File
@@ -6,23 +6,40 @@ title: Register Filters/Tags
```typescript ```typescript
// Usage: {% upper name %} // Usage: {% upper name %}
import { Value, Tag, TagToken, Context, TopLevelToken, Liquid } from 'liquidjs' import { Value, TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
engine.registerTag('upper', class UpperTag extends Tag { engine.registerTag('upper', {
private value: Value parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) { this.value = new Value(tagToken.args, engine)
super(tagToken, remainTokens, liquid) },
this.value = new Value(tagToken.args, liquid) render: function*(ctx: Context) {
} const str = yield this.value.value(ctx); // 'alice'
* render(ctx: Context) {
const str = yield this.value.value(ctx) // 'alice'
return str.toUpperCase() // 'ALICE' return str.toUpperCase() // 'ALICE'
} }
}); });
``` ```
* `constructor`: Parse tag arguments and read tokens from `remainTokens` until your end token. `liquid` is passed as the third argument. * `parse`: Read tokens from `remainTokens` until your end token.
* `render`: Return an HTML string (or `return yield` a value) for simple tags that produce one value; use `emitter.write()` when writing incrementally or delegating via `yield this.liquid.renderer.renderTemplates()`, since nested templates write through the shared emitter. * `render`: Combine scope data with your parsed tokens into HTML string.
For complex tag implementation, you can also provide a tag class:
```typescript
// Usage: {% upper name:"alice" %}
import { Hash, Tag, TagToken, Context, Emitter, TopLevelToken, Liquid } from 'liquidjs'
engine.registerTag('upper', class UpperTag extends Tag {
private hash: Hash
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.hash = new Hash(tagToken.args)
}
* render(ctx: Context) {
const hash = yield this.hash.render();
return hash.name.toUpperCase() // 'ALICE'
}
});
```
See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/tags> See existing tag implementations here: <https://github.com/harttle/liquidjs/tree/master/src/tags>
See demo example here: https://github.com/harttle/liquidjs/blob/master/demo/typescript/index.ts See demo example here: https://github.com/harttle/liquidjs/blob/master/demo/typescript/index.ts
@@ -47,17 +64,14 @@ See existing filter implementations here: <https://github.com/harttle/liquidjs/t
In some cases it's desirable to disable some tags/filters (see [#324](https://github.com/harttle/liquidjs/issues/324)). You'll need to register a dummy tag/filter that throws a corresponding Error. In some cases it's desirable to disable some tags/filters (see [#324](https://github.com/harttle/liquidjs/issues/324)). You'll need to register a dummy tag/filter that throws a corresponding Error.
```typescript ```javascript
import { Tag } from 'liquidjs'
// disable a tag // disable a tag
engine.registerTag('include', class extends Tag { const disabledTag = {
constructor(token, remainTokens, liquid) { parse: function(token) {
super(token, remainTokens, liquid) throw new Error(`tag "${token.name}" disabled`);
throw new Error(`tag "${token.name}" disabled`)
} }
render() {} }
}) engine.registerTag('include', disabledTag);
// disable a filter // disable a filter
function disabledFilter(name) { function disabledFilter(name) {
+15 -22
View File
@@ -22,7 +22,7 @@ Expected output:
</div> </div>
``` ```
Firstly, [register][register-tags] a tag named `wrap` and parse the content into `this.tpls`. In the tag `constructor(tagToken, remainTokens, liquid)`: Firstly, [register][register-tags] a tag named `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`:
- `tagToken` is current token `{%raw%}{% wrap %}{%endraw%}`, and - `tagToken` is current token `{%raw%}{% wrap %}{%endraw%}`, and
- `remainTokens` is an array of all tokens following `{%raw%}{% wrap %}{%endraw%}` until the end of this template file. - `remainTokens` is an array of all tokens following `{%raw%}{% wrap %}{%endraw%}` until the end of this template file.
@@ -30,14 +30,11 @@ Firstly, [register][register-tags] a tag named `wrap` and parse the content into
Basically, what we need to do is take/`.shift()` enough tags from `remainTokens` until we get an `endwrap` token (the name can be arbitrary, but by convention it should be `endwrap`). And if there's no `endwrap` until the end of the template file, we need to throw a tag-not-closed `Error`. Basically, what we need to do is take/`.shift()` enough tags from `remainTokens` until we get an `endwrap` token (the name can be arbitrary, but by convention it should be `endwrap`). And if there's no `endwrap` until the end of the template file, we need to throw a tag-not-closed `Error`.
```javascript ```javascript
const { Tag } = require('liquidjs') engine.registerTag('wrap', {
parse(tagToken, remainTokens) {
engine.registerTag('wrap', class WrapTag extends Tag {
constructor(tagToken, remainTokens, liquid) {
super(tagToken, remainTokens, liquid)
this.tpls = [] this.tpls = []
let closed = false let closed = false
while (remainTokens.length) { while(remainTokens.length) {
let token = remainTokens.shift() let token = remainTokens.shift()
// we got the end tag! stop taking tokens // we got the end tag! stop taking tokens
if (token.name === 'endwrap') { if (token.name === 'endwrap') {
@@ -47,11 +44,11 @@ engine.registerTag('wrap', class WrapTag extends Tag {
// parse token into template // parse token into template
// parseToken() may consume more than 1 tokens // parseToken() may consume more than 1 tokens
// e.g. {% if %}...{% endif %} // e.g. {% if %}...{% endif %}
let tpl = liquid.parser.parseToken(token, remainTokens) let tpl = this.liquid.parser.parseToken(token, remainTokens)
this.tpls.push(tpl) this.tpls.push(tpl)
} }
if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`) if (!closed) throw new Error(`tag ${tagToken.getText()} not closed`)
} },
* render(context, emitter) { * render(context, emitter) {
emitter.write("<div class='wrapper'>") emitter.write("<div class='wrapper'>")
yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter) yield this.liquid.renderer.renderTemplates(this.tpls, context, emitter)
@@ -60,17 +57,16 @@ engine.registerTag('wrap', class WrapTag extends Tag {
}) })
``` ```
`.renderTemplates()` can be async; we need `yield` to wait for it to complete. For more details on async in LiquidJS, see [Sync and Async][async]. Here's a JSFiddle version: <https://jsfiddle.net/por0zcn1/3/> `.renderTemplates()` can be async; we need `yield` to wait for it to complete. For more details on async in LiquidJS, see [Sync and Async][async]. Other parts of the `render()` method are quite straightforward. Here's a JSFiddle version: <https://jsfiddle.net/por0zcn1/3/>
## Using ParseStream ## Using ParseStream
For more complex tags such as [for][for] and [if][if], constructor parsing can get unwieldy. [ParseStream][ParseStream] offers an event-based API for this. The constructor below is equivalent to the example above: 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` that does exactly the same as the example above.
```javascript ```javascript
constructor(tagToken, remainTokens, liquid) { parse(tagToken, remainTokens) {
super(tagToken, remainTokens, liquid)
this.tpls = [] this.tpls = []
liquid.parser.parseStream(remainTokens) this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl)) .on('template', tpl => this.tpls.push(tpl))
// note that we cannot use arrow function because we need `this` // note that we cannot use arrow function because we need `this`
.on('tag:endwrap', function () { this.stop() }) .on('tag:endwrap', function () { this.stop() })
@@ -107,18 +103,15 @@ As you've noticed, there's an additional `repeat.i` in the context of `repeat`.
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: 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 ```javascript
const { Tag } = require('liquidjs') engine.registerTag('repeat', {
parse(tagToken, remainTokens) {
engine.registerTag('repeat', class RepeatTag extends Tag {
constructor(tagToken, remainTokens, liquid) {
super(tagToken, remainTokens, liquid)
this.tpls = [] this.tpls = []
liquid.parser.parseStream(remainTokens) this.liquid.parser.parseStream(remainTokens)
.on('template', tpl => this.tpls.push(tpl)) .on('template', tpl => this.tpls.push(tpl))
.on('tag:endrepeat', function () { this.stop() }) .on('tag:endrepeat', function () { this.stop() })
.on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) }) .on('end', () => { throw new Error(`tag ${tagToken.getText()} not closed`) })
.start() .start()
} },
* render(context, emitter) { * render(context, emitter) {
const repeat = { i: 1 } const repeat = { i: 1 }
context.push({ repeat }) context.push({ repeat })
@@ -130,7 +123,7 @@ engine.registerTag('repeat', class RepeatTag extends Tag {
}) })
``` ```
The constructor is exactly the same as `wrap` tag, we repeat the content simply by calling `.renderTemplates(this.tpls)` twice during `render()`. Here's the JSFiddle: <https://jsfiddle.net/por0zcn1/2/> 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: <https://jsfiddle.net/por0zcn1/2/>
{% note warn Use Push & Pop in Pairs %} {% 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. `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.
-2
View File
@@ -50,8 +50,6 @@ All built-in tags are implemented this way and are safe to use in both sync and
- do not directly `return <Promise>`, and - do not directly `return <Promise>`, and
- do not call any APIs that return a Promise. - do not call any APIs that return a Promise.
You can write output with `emitter.write()` or `return` / `return yield` an HTML string — both are emitted to output. Returning is handy for simple tags that produce one value (for example `{% cycle %}`); use `emitter.write()` when writing output incrementally or when delegating via `yield renderTemplates()`, since nested templates write through the shared emitter.
## Call APIs that return a Promise ## Call APIs that return a Promise
But LiquidJS is Promise-friendly, right? You can still call Promise-based functions and wait for that Promise within tag implementations. Just replace `await` with `yield`. e.g. we're calling `fs.readFile()` which returns a `Promise`: But LiquidJS is Promise-friendly, right? You can still call Promise-based functions and wait for that Promise within tag implementations. Just replace `await` with `yield`. e.g. we're calling `fs.readFile()` which returns a `Promise`: