From d0b61fc5fe7058085f0992bdf19bce2e040c323d Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Tue, 14 Jul 2026 21:41:46 +0800 Subject: [PATCH] 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 --- .../source/tutorials/register-filters-tags.md | 54 ++++++++++++------- docs/source/tutorials/render-tag-content.md | 37 ++++++------- docs/source/tutorials/sync-and-async.md | 2 - 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/docs/source/tutorials/register-filters-tags.md b/docs/source/tutorials/register-filters-tags.md index e15bdd7de..2e6bf7d33 100644 --- a/docs/source/tutorials/register-filters-tags.md +++ b/docs/source/tutorials/register-filters-tags.md @@ -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: See demo example here: https://github.com/harttle/liquidjs/blob/master/demo/typescript/index.ts @@ -47,17 +64,14 @@ See existing filter implementations here: ``` -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("
") 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: +`.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: ## 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: +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. diff --git a/docs/source/tutorials/sync-and-async.md b/docs/source/tutorials/sync-and-async.md index 69acafa30..f47c48d38 100644 --- a/docs/source/tutorials/sync-and-async.md +++ b/docs/source/tutorials/sync-and-async.md @@ -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 `, 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`: