feat!: drop TagImplOptions in favor of Tag classes (#839) (#927)

* feat!: drop TagImplOptions in favor of Tag classes (#839)

Remove tag-options-adapter and the registerTag object-literal overload.
Custom tags must extend Tag.

Co-authored-by: Cursor <[email protected]>

* test: drop TagImplOptions-specific e2e coverage (#839)

Remove #570 v9 object-literal registration test and unused metadata_file setup in #573.

Co-authored-by: Cursor <[email protected]>

* test: use inline Tag classes in register-tags spec

Co-authored-by: Cursor <[email protected]>

* test: remove dead throwingTag setup and duplicate throw stub

for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag.

Co-authored-by: Cursor <[email protected]>

* test: remove dead throwingTag setup and duplicate throw stub

for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag.

Co-authored-by: Cursor <[email protected]>

* fix(demo): ignore killall exit when express server already stopped

Co-authored-by: Cursor <[email protected]>

* fix(demo): revert unrelated return->exit change in express test

Co-authored-by: Cursor <[email protected]>

* fix(demo): use exit in express test script (no enclosing function)

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-07-09 00:58:43 +08:00
co-authored by Cursor
parent 5e3928654b
commit cc4a9ce0a7
11 changed files with 78 additions and 99 deletions
+2 -2
View File
@@ -14,9 +14,9 @@ for demo in $(ls demo); do
npm link liquidjs
if npm test; then
echo [success] demo/webpack
echo "[success] demo/$demo"
else
echo [fail] demo/webpack
echo "[fail] demo/$demo"
exit 1
fi
cd -
+1 -26
View File
@@ -1,4 +1,4 @@
import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..'
import { Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..'
import { spawnSync } from 'child_process'
import { resolve as resolvePath } from 'path'
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
@@ -362,33 +362,8 @@ describe('Issues', function () {
const html = await liquid.parseAndRender(tpl)
expect(html).toMatch(/^\s*This is a love or luck potion.\s+This is a strength or health or love potion.\s*$/)
})
it('tag registration compatible to v9 #570', async () => {
const liquid = new Liquid()
liquid.registerTag('metadata_file', {
parse (tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args
},
async render (ctx: Context) {
const content = await Promise.resolve(`{{${this.str}}}`)
return this.liquid.parseAndRender(content.toString(), ctx)
}
})
const tpl = '{% metadata_file foo %}'
const ctx = { foo: 'FOO' }
const html = await liquid.parseAndRender(tpl, ctx)
expect(html).toBe('FOO')
})
it('date filter should return parsed input when no format is provided #573', async () => {
const liquid = new Liquid()
liquid.registerTag('metadata_file', {
parse (tagToken: TagToken, remainTokens: TopLevelToken[]) {
this.str = tagToken.args
},
async render (ctx: Context) {
const content = await Promise.resolve(`{{${this.str}}}`)
return this.liquid.parseAndRender(content.toString(), ctx)
}
})
const tpl = `{{ 'now' | date }}`
const html = await liquid.parseAndRender(tpl)
// sample: Thursday, February 2, 2023 at 6:25 pm +0000
+3 -6
View File
@@ -1,6 +1,7 @@
import { Liquid, Context, isFalsy } from '../../../src'
import { mock, restore } from '../../stub/mockfs'
import { drainStream } from '../../stub/stream'
import { ThrowingTag } from '../../stub/tags'
import { resolve } from 'path'
describe('Liquid', function () {
@@ -231,11 +232,7 @@ describe('Liquid', function () {
'/root/error.html': 'A{%throwingTag%}B'
})
engine = new Liquid({ root: ['/root/'] })
engine.registerTag('throwingTag', {
render: function () {
throw new Error('intended render error')
}
})
engine.registerTag('throwingTag', ThrowingTag)
})
afterEach(restore)
it('should render a simple value', async () => {
@@ -244,7 +241,7 @@ describe('Liquid', function () {
})
it('should throw RenderError when tag throws', async () => {
const stream = await engine.renderFileToNodeStream('error.html')
expect(drainStream(stream)).rejects.toThrow(/intended render error/)
expect(drainStream(stream)).rejects.toThrow(/intended error/)
})
})
describe('#analyze', () => {
+32 -13
View File
@@ -1,38 +1,57 @@
import { Liquid } from '../../../src/liquid'
import { Tag } from '../../../src/template/tag'
import type { Context } from '../../../src/context'
import type { TagToken, TopLevelToken } from '../../../src/tokens'
describe('liquid#registerTag()', function () {
it('should support render to simple string', async () => {
class SimpleStringTag extends Tag {
render () {
return 'B'
}
}
const liquid = new Liquid()
liquid.registerTag('simple-string', {
render: () => 'B'
})
liquid.registerTag('simple-string', SimpleStringTag)
const html = await liquid.parseAndRender(`A{% simple-string %}C`)
return expect(html).toBe('ABC')
})
it('should support async tag render', async () => {
class AsyncStringTag extends Tag {
async render () {
return 'B'
}
}
const liquid = new Liquid()
liquid.registerTag('async-string', {
render: async () => 'B'
})
liquid.registerTag('async-string', AsyncStringTag)
const html = await liquid.parseAndRender(`A{% async-string %}C`)
return expect(html).toBe('ABC')
})
it('should have access to ctx in render()', async () => {
class DynamicStringTag extends Tag {
async render (ctx: Context) {
return ctx.get(['c'])
}
}
const liquid = new Liquid()
liquid.registerTag('dynamic-string', {
render: async (ctx) => ctx.get(['c'])
})
liquid.registerTag('dynamic-string', DynamicStringTag)
const html = await liquid.parseAndRender(`A{% dynamic-string %}C`, {
c: 'B'
})
return expect(html).toBe('ABC')
})
it('should have access to tag arguments', async () => {
class ArgumentReflectorTag extends Tag {
variable: string
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.variable = token.args.split('=')[1]
}
async render (ctx: Context) {
return ctx.get([this.variable])
}
}
const liquid = new Liquid()
liquid.registerTag('argument-reflector', {
parse: function (token) { this.variable = token.args.split('=')[1] },
render: async function (ctx) { return ctx.get(this.variable) }
})
liquid.registerTag('argument-reflector', ArgumentReflectorTag)
const html = await liquid.parseAndRender(`A{% argument-reflector variable=c %}C`, {
c: 'B'
})
+8 -14
View File
@@ -2,7 +2,8 @@ import { RenderError } from '../../../src/util/error'
import { Liquid } from '../../../src/liquid'
import { resolve } from 'path'
import { mock, restore } from '../../stub/mockfs'
import { throwIntendedError, rejectIntendedError } from '../../stub/util'
import { throwIntendedError } from '../../stub/util'
import { ThrowingTag, RejectingTag, ThrowsOnParseTag } from '../../stub/tags'
const strictEngine = new Liquid({
strictVariables: true,
@@ -13,9 +14,9 @@ const strictCatchingEngine = new Liquid({
strictVariables: true,
strictFilters: true
})
strictEngine.registerTag('throwingTag', { render: throwIntendedError })
strictEngine.registerTag('throwingTag', ThrowingTag)
strictEngine.registerFilter('throwingFilter', throwIntendedError)
strictCatchingEngine.registerTag('throwingTag', { render: throwIntendedError })
strictCatchingEngine.registerTag('throwingTag', ThrowingTag)
strictCatchingEngine.registerFilter('throwingFilter', throwIntendedError)
describe('error', function () {
@@ -83,8 +84,8 @@ describe('error', function () {
engine = new Liquid({
root: '/'
})
engine.registerTag('throwingTag', { render: throwIntendedError })
engine.registerTag('rejectingTag', { render: rejectIntendedError })
engine.registerTag('throwingTag', ThrowingTag)
engine.registerTag('rejectingTag', RejectingTag)
engine.registerFilter('throwingFilter', throwIntendedError)
})
it('should throw RenderError when tag throws', async function () {
@@ -244,10 +245,7 @@ describe('error', function () {
let engine: Liquid
beforeEach(function () {
engine = new Liquid()
engine.registerTag('throwsOnParse', {
parse: throwIntendedError,
render: () => ''
})
engine.registerTag('throwsOnParse', ThrowsOnParseTag)
})
it('should throw ParseError when filter not defined', async function () {
await expect(strictEngine.parseAndRender('{{1 | a}}')).rejects.toMatchObject({
@@ -337,11 +335,7 @@ describe('error', function () {
engine = new Liquid({
root: '/'
})
engine.registerTag('throwingTag', {
render: function () {
throw new Error('intended error')
}
})
engine.registerTag('throwingTag', ThrowingTag)
})
it('should throw RenderError when tag throws', function () {
const src = '{%throwingTag%}'
-3
View File
@@ -7,9 +7,6 @@ describe('tags/for', function () {
let liquid: Liquid, scope: Scope
beforeEach(function () {
liquid = new Liquid()
liquid.registerTag('throwingTag', {
render: function () { throw new Error('intended render error') }
})
scope = {
one: 1,
// eslint-disable-next-line
+26
View File
@@ -0,0 +1,26 @@
import { throwIntendedError, rejectIntendedError } from './util'
import { Tag } from '../../src/template/tag'
import type { TagToken, TopLevelToken } from '../../src/tokens'
import type { Liquid } from '../../src/liquid'
export class ThrowingTag extends Tag {
render () {
throwIntendedError()
}
}
export class RejectingTag extends Tag {
async render () {
await rejectIntendedError()
}
}
export class ThrowsOnParseTag extends Tag {
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
throwIntendedError()
}
render () {
return ''
}
}