From 8d98d89486a1915caeefa88062bd0b9d9ba356f8 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Tue, 14 Jul 2026 21:44:47 +0800 Subject: [PATCH] docs: trim security-model prose and update render-tag-content Remove diary-style engine comparisons from security-model.md. Update render-tag-content tutorial to Tag class examples with tpls class field. Co-authored-by: Cursor --- docs/source/tutorials/render-tag-content.md | 43 ++++++++++++--------- docs/source/tutorials/security-model.md | 6 +-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/docs/source/tutorials/render-tag-content.md b/docs/source/tutorials/render-tag-content.md index 33195b090..cf655207f 100644 --- a/docs/source/tutorials/render-tag-content.md +++ b/docs/source/tutorials/render-tag-content.md @@ -22,7 +22,7 @@ Expected output: ``` -Firstly, [register][register-tags] a tag named `wrap` and parse the content into `this.tpls`. Here in `parse(tagToken, remainTokens)`: +Firstly, [register][register-tags] a tag named `wrap` and parse the content into `this.tpls`. In the tag `constructor(tagToken, remainTokens, liquid)`: - `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,11 +30,14 @@ 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 -engine.registerTag('wrap', { - parse(tagToken, remainTokens) { - this.tpls = [] +const { Tag } = require('liquidjs') + +engine.registerTag('wrap', class WrapTag extends Tag { + tpls = [] + constructor(tagToken, remainTokens, liquid) { + super(tagToken, remainTokens, liquid) 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') { @@ -44,11 +47,11 @@ engine.registerTag('wrap', { // parse token into template // parseToken() may consume more than 1 tokens // e.g. {% if %}...{% endif %} - let tpl = this.liquid.parser.parseToken(token, remainTokens) + let tpl = 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) @@ -57,16 +60,17 @@ engine.registerTag('wrap', { }) ``` -`.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: +`.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: ## 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` that does exactly the same as the example above. +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: ```javascript -parse(tagToken, remainTokens) { - this.tpls = [] - this.liquid.parser.parseStream(remainTokens) +tpls = [] +constructor(tagToken, remainTokens, liquid) { + super(tagToken, remainTokens, liquid) + 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() }) @@ -103,15 +107,18 @@ 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 -engine.registerTag('repeat', { - parse(tagToken, remainTokens) { - this.tpls = [] - this.liquid.parser.parseStream(remainTokens) +const { Tag } = require('liquidjs') + +engine.registerTag('repeat', class RepeatTag extends Tag { + tpls = [] + constructor(tagToken, remainTokens, liquid) { + super(tagToken, remainTokens, liquid) + 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 }) @@ -123,7 +130,7 @@ engine.registerTag('repeat', { }) ``` -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: +The constructor is the same as in the `wrap` tag; we repeat the content 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/security-model.md b/docs/source/tutorials/security-model.md index 9555742fb..87098b347 100644 --- a/docs/source/tutorials/security-model.md +++ b/docs/source/tutorials/security-model.md @@ -12,10 +12,6 @@ The built-in limits are cooperative safeguards, not strict runtime isolation. - They do **not** sandbox JavaScript execution. - They should be combined with process/container limits and request timeouts for defense in depth. -LiquidJS does **not** enforce memory or CPU budgets inside the engine. Major template engines take the same approach: byte-level heap tracking is unreliable in garbage-collected runtimes (non-deterministic GC, accounting overhead) and does not map cleanly to real process memory. [Jinja2](https://jinja.palletsprojects.com/en/stable/sandbox/) relies on `sys.setrecursionlimit`, `SandboxedEnvironment` for access control, and advises OS/process limits (`ulimit`, cgroups). [Twig](https://twig.symfony.com/doc/3.x/api.html#security-policy) documents a `SecurityPolicy` for tags/filters/methods and explicitly leaves resource limits to PHP (`memory_limit`, execution timeouts). Handlebars and EJS provide no render budgets; Node.js users typically combine [`vm.Script` timeouts](https://nodejs.org/api/vm.html) or [`worker_threads`](https://nodejs.org/api/worker_threads.html) with process isolation for untrusted templates. - -For LiquidJS in production, prefer **external** controls: Node.js `vm` or worker threads (or packages such as [`isolated-vm`](https://www.npmjs.com/package/isolated-vm) when stronger isolation is required), separate processes or containers, OS/container memory and CPU quotas, and request timeouts — not in-engine heap tracking. - ## Limits at a glance - [parseLimit][parseLimit]: limit total template size per `parse()` call. @@ -53,7 +49,7 @@ Each template node (the `for` tag, literal `order: `, output `{{i}}`, and so on) [maxDepth][maxDepth] limits how deeply `{% render %}`, `{% include %}`, and `{% layout %}` can nest. Defaults to `128`. -Memory-heavy templates (for example exponential `concat` in a loop) are not capped by LiquidJS. Mitigate them with process/container memory limits, output size checks after render, or template restrictions — the same pattern Jinja2 and Twig recommend for heap and CPU. +The `memoryLimit` option was removed; memory usage is not capped in-engine. ## `ownPropertyOnly` and scope data