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
// 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 {
private value: Value
constructor(tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.value = new Value(tagToken.args, liquid)
}
* render(ctx: Context) {
const str = yield this.value.value(ctx) // 'alice'
engine.registerTag('upper', {
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.value = new Value(tagToken.args, engine)
},
render: function*(ctx: Context) {
const str = yield this.value.value(ctx); // '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.
* `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.
* `parse`: Read tokens from `remainTokens` until your end token.
* `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 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.
```typescript
import { Tag } from 'liquidjs'
```javascript
// disable a tag
engine.registerTag('include', class extends Tag {
constructor(token, remainTokens, liquid) {
super(token, remainTokens, liquid)
throw new Error(`tag "${token.name}" disabled`)
const disabledTag = {
parse: function(token) {
throw new Error(`tag "${token.name}" disabled`);
}
render() {}
})
}
engine.registerTag('include', disabledTag);
// disable a filter
function disabledFilter(name) {
+15 -22
View File
@@ -22,7 +22,7 @@ Expected output:
</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
- `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`.
```javascript
const { Tag } = require('liquidjs')
engine.registerTag('wrap', class WrapTag extends Tag {
constructor(tagToken, remainTokens, liquid) {
super(tagToken, remainTokens, liquid)
engine.registerTag('wrap', {
parse(tagToken, remainTokens) {
this.tpls = []
let closed = false
while (remainTokens.length) {
while(remainTokens.length) {
let token = remainTokens.shift()
// we got the end tag! stop taking tokens
if (token.name === 'endwrap') {
@@ -47,11 +44,11 @@ engine.registerTag('wrap', class WrapTag extends Tag {
// parse token into template
// parseToken() may consume more than 1 tokens
// e.g. {% if %}...{% endif %}
let tpl = liquid.parser.parseToken(token, remainTokens)
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)
@@ -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
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
constructor(tagToken, remainTokens, liquid) {
super(tagToken, remainTokens, liquid)
parse(tagToken, remainTokens) {
this.tpls = []
liquid.parser.parseStream(remainTokens)
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() })
@@ -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:
```javascript
const { Tag } = require('liquidjs')
engine.registerTag('repeat', class RepeatTag extends Tag {
constructor(tagToken, remainTokens, liquid) {
super(tagToken, remainTokens, liquid)
engine.registerTag('repeat', {
parse(tagToken, remainTokens) {
this.tpls = []
liquid.parser.parseStream(remainTokens)
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 })
@@ -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 %}
`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 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
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`: