Compare commits

...
Author SHA1 Message Date
Yang JunandCursor 6111eb6bd9 feat: remove downlevelIteration from Rollup tsconfig override (#905)
Browser UMD/min bundles already compile with ES2020 on next; drop the
ES5-only downlevelIteration override from rollup.config.mjs.

Co-authored-by: Cursor <[email protected]>
2026-07-10 23:16:56 +08:00
9481008f2b feat: drop keepOutputType option (#838) (#930)
Remove KeepingTypeEmitter and always stringify via SimpleEmitter. On next, map breaking commits to patch so alpha stays on 11.x.

Co-authored-by: Cursor <[email protected]>
2026-07-09 23:13:19 +08:00
Yang JunandCursor cc4a9ce0a7 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]>
2026-07-09 00:58:43 +08:00
Yang JunandCursor 5e3928654b chore(release): setup v11 alpha publishing on next
Co-authored-by: Cursor <[email protected]>
2026-07-09 00:58:43 +08:00
Yang Jun 962e5b6433 chore: update to Node.js LTS 2026-07-09 00:58:42 +08:00
33 changed files with 158 additions and 313 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: 'latest'
- name: Install dependencies
run: npm ci
- name: Build
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '22'
node-version: 'latest'
- name: Install dependencies
run: npm ci
- name: Test
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: 'latest'
- name: Build
run: |
npm ci
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: 'latest'
- name: Install dependencies
run: npm ci
- name: Lint
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: 'latest'
- name: Download artifacts
uses: actions/download-artifact@v4
with:
+1 -6
View File
@@ -24,12 +24,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ ${{ github.ref == 'refs/heads/master' }} ]; then
npx semantic-release
else
npx semantic-release --dry-run
fi
run: npx semantic-release
- name: Archive npm failure logs
uses: actions/upload-artifact@v4
if: failure()
+4 -14
View File
@@ -7,20 +7,10 @@ jobs:
matrix:
os: [ubuntu-latest]
timezone: [Etc/GMT, Asia/Shanghai, America/New_York]
node-version: [22]
node-version: [latest]
include:
- os: macos-latest
timezone: America/New_York
node-versoin: 22
- os: ubuntu-latest
timezone: Etc/GMT
node-version: 20
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 18
- os: ubuntu-latest
timezone: Asia/Shanghai
node-version: 16
node-version: lts/*
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -39,7 +29,7 @@ jobs:
name: dist-${{ matrix.os }}
path: dist
- name: Run Test
run: TZ=${{ matrix.timezone }} npm test
run: TZ=${{ matrix.timezone || 'Etc/GMT' }} npm test
- name: Archive npm failure logs
uses: actions/upload-artifact@v4
if: failure()
@@ -57,7 +47,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 22
node-version: latest
- name: Download artifacts
uses: actions/download-artifact@v4
with:
+50
View File
@@ -0,0 +1,50 @@
const gitPlugin = [
'@semantic-release/git',
{
assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
}
]
const githubPlugin = [
'@semantic-release/github',
{
assets: [
{ path: 'dist/*.umd.js', label: 'liquid.js' },
{ path: 'dist/*.min.js', label: 'liquid.min.js' },
{ path: 'dist/*.min.js.map', label: 'liquid.min.js.map' }
]
}
]
// next is branch-protected (PR-only); skip @semantic-release/git there and publish to npm only.
const onMaster = process.env.GITHUB_REF === 'refs/heads/master'
const onNext = process.env.GITHUB_REF === 'refs/heads/next'
// On next, breaking changes are v11 WIP — bump alpha prerelease only, not major.
const commitAnalyzer = onNext
? ['@semantic-release/commit-analyzer', {
releaseRules: [
{ breaking: true, release: 'patch' }
]
}]
: '@semantic-release/commit-analyzer'
const basePlugins = [
commitAnalyzer,
'@semantic-release/release-notes-generator',
'@semantic-release/changelog',
'@semantic-release/npm'
]
module.exports = {
branches: [
'master',
{ name: 'next', prerelease: 'alpha' }
],
plugins: [
...basePlugins,
...(onMaster ? [gitPlugin] : []),
githubPlugin
]
}
+2 -2
View File
@@ -7,13 +7,13 @@ while ! grep -q "Express running" "$LOG_FILE"; do
if ! kill -0 $SERVER_PID; then
echo "Server exited unexpectedly."
cat $LOG_FILE
return 1
exit 1
fi
sleep 1
done
curl http://127.0.0.1:3000 | grep -q 'Welcome to LiquidJS'
RESULT=$?
killall node
killall node || true
rm $LOG_FILE
if [ $RESULT != 0 ]; then
exit 1
+1 -1
View File
@@ -64,7 +64,7 @@
"typescript": "^4.5.3"
},
"engines": {
"node": ">=16"
"node": ">=20"
},
"funding": {
"type": "opencollective",
+1 -40
View File
@@ -12,7 +12,7 @@
},
"types": "dist/index.d.ts",
"engines": {
"node": ">=16"
"node": ">=20"
},
"scripts": {
"lint": "eslint \"**/*.mjs\" \"**/*.ts\" .",
@@ -121,45 +121,6 @@
"dependencies": {
"commander": "^10.0.0"
},
"release": {
"branch": "master",
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/git",
{
"assets": [
"package.json",
"package-lock.json",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "dist/*.umd.js",
"label": "liquid.js"
},
{
"path": "dist/*.min.js",
"label": "liquid.min.js"
},
{
"path": "dist/*.min.js.map",
"label": "liquid.min.js.map"
}
]
}
]
]
},
"publishConfig": {
"provenance": true
},
+5 -6
View File
@@ -23,8 +23,7 @@ const tsconfig = (target) => ({
compilerOptions: {
target,
module: 'ES2015',
rootDir: 'src',
downlevelIteration: true
rootDir: 'src'
}
}
})
@@ -89,7 +88,7 @@ const nodeEsm = {
plugins: [
versionInjection,
replace(esmRequire),
typescript(tsconfig('es6'))
typescript(tsconfig('ES2020'))
],
treeshake,
input
@@ -108,7 +107,7 @@ const browserEsm = {
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es6'))
typescript(tsconfig('ES2020'))
],
treeshake,
input
@@ -128,7 +127,7 @@ const browserUmd = {
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es5'))
typescript(tsconfig('ES2020'))
],
treeshake,
input
@@ -148,7 +147,7 @@ const browserMin = {
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es5')),
typescript(tsconfig('ES2020')),
uglify()
],
treeshake,
-5
View File
@@ -2,11 +2,6 @@ import * as base64 from './base64-impl-browser'
import { JSDOM } from 'jsdom'
describe('base64-impl/browser', function () {
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping base64-impl-browser...')
return
}
beforeEach(function () {
const dom = new JSDOM(``, {
url: 'https://example.com/',
-4
View File
@@ -3,10 +3,6 @@ import * as sinon from 'sinon'
import { JSDOM } from 'jsdom'
describe('fs/browser', function () {
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping template-browser...')
return
}
beforeEach(function () {
const dom = new JSDOM(``, {
url: 'https://example.com/foo/bar/',
-1
View File
@@ -1,4 +1,3 @@
export * from './emitter'
export * from './simple-emitter'
export * from './streamed-emitter'
export * from './keeping-type-emitter'
-19
View File
@@ -1,19 +0,0 @@
import { stringify, toValue } from '../util'
import { Emitter } from './emitter'
export class KeepingTypeEmitter implements Emitter {
public buffer: any = '';
public write (html: any) {
html = toValue(html)
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (typeof html !== 'string' && this.buffer === '') {
this.buffer = html
} else {
this.buffer = stringify(this.buffer) + stringify(html)
}
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ export function * sort_natural<T> (this: FilterImpl, arr: T[], property?: string
return yield * sortBy.call(this, arr, property, caseInsensitiveCompare)
}
export const size = (v: string | any[]) => (v && v.length) || 0
export const size = (v: string | any[]) => v?.length || 0
export function * map (this: FilterImpl, arr: Scope[], property: string): IterableIterator<unknown> {
const results = []
+4 -8
View File
@@ -79,8 +79,6 @@ export interface LiquidOptions {
templates?: {[key: string]: string};
/** the global scope passed down to all partial and layout templates, i.e. templates included by `include`, `layout` and `render` tags. */
globals?: object;
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
keepOutputType?: boolean;
/** Default escape filter applied to output values, when set, you'll have to add `| raw` for values don't need to be escaped. Defaults to `undefined`. */
outputEscape?: OutputEscapeOption;
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
@@ -160,7 +158,6 @@ export interface NormalizedFullOptions extends NormalizedOptions {
preserveTimezones: boolean;
greedy: boolean;
globals: object;
keepOutputType: boolean;
operators: Operators;
parseLimit: number;
renderLimit: number;
@@ -196,7 +193,6 @@ export const defaultOptions: NormalizedFullOptions = {
ownPropertyOnly: true,
lenientIf: false,
globals: {},
keepOutputType: false,
operators: defaultOperators,
memoryLimit: Infinity,
parseLimit: Infinity,
@@ -204,11 +200,11 @@ export const defaultOptions: NormalizedFullOptions = {
}
export function normalize (options: LiquidOptions): NormalizedFullOptions {
if (options.hasOwnProperty('root')) {
if (!options.hasOwnProperty('partials')) options.partials = options.root
if (!options.hasOwnProperty('layouts')) options.layouts = options.root
if ('root' in options) {
if (!('partials' in options)) options.partials = options.root
if (!('layouts' in options)) options.layouts = options.root
}
if (options.hasOwnProperty('cache')) {
if ('cache' in options) {
let cache: LiquidCache | undefined
if (typeof options.cache === 'number') cache = options.cache > 0 ? new LRU(options.cache) : undefined
else if (typeof options.cache === 'object') cache = options.cache
+4 -4
View File
@@ -1,6 +1,6 @@
import { Context } from './context'
import { toPromise, toValueSync, isFunction, forOwn, isString, strictUniq } from './util'
import { TagClass, createTagClass, TagImplOptions, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, SegmentArray } from './template'
import { toPromise, toValueSync, forOwn, isString, strictUniq } from './util'
import { TagClass, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, SegmentArray } from './template'
import { LookupType } from './fs/loader'
import { Render } from './render'
import { Parser } from './parser'
@@ -101,8 +101,8 @@ export class Liquid {
public registerFilter (name: string, filter: FilterImplOptions) {
this.filters[name] = filter
}
public registerTag (name: string, tag: TagClass | TagImplOptions) {
this.tags[name] = isFunction(tag) ? tag : createTagClass(tag)
public registerTag (name: string, tag: TagClass) {
this.tags[name] = tag
}
public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) {
return plugin.call(this, Liquid)
+2 -2
View File
@@ -2,7 +2,7 @@ import { getPerformance } from '../util/performance'
import { toPromise, RenderError, LiquidErrors, LiquidError } from '../util'
import { Context } from '../context'
import { Template } from '../template'
import { Emitter, KeepingTypeEmitter, StreamedEmitter, SimpleEmitter } from '../emitters'
import { Emitter, StreamedEmitter, SimpleEmitter } from '../emitters'
export class Render {
public renderTemplatesToNodeStream (templates: Template[], ctx: Context): NodeJS.ReadableStream {
@@ -13,7 +13,7 @@ export class Render {
}
public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> {
if (!emitter) {
emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter()
emitter = new SimpleEmitter()
}
ctx.renderLimit.check(getPerformance().now())
const errors = []
-1
View File
@@ -1,7 +1,6 @@
export * from './template'
export * from './template-impl'
export * from './tag'
export * from './tag-options-adapter'
export * from './filter'
export * from './filter-impl-options'
export * from './hash'
-56
View File
@@ -2,7 +2,6 @@ import { toPromise } from '../util'
import { Context } from '../context'
import { Output } from '../template'
import { OutputToken } from '../tokens'
import { defaultOptions } from '../liquid-options'
describe('Output', function () {
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
@@ -30,59 +29,4 @@ describe('Output', function () {
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('FOO')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output(token, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('FOO')
})
describe('when keepOutputType is enabled', () => {
const emitter: any = {
write: (html: any) => {
if (emitter.keepOutputType && typeof html !== 'string') {
emitter.html = html
} else {
emitter.html += html as string
}
},
html: '',
keepOutputType: true
}
const token = { content: 'foo', input: 'foo' } as OutputToken
beforeEach(() => { emitter.html = '' })
it('should respect output variable number type', async () => {
const scope = new Context({
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output(token, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe(42)
})
it('should respect output variable boolean type', async () => {
const scope = new Context({
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output(token, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe(true)
})
it('should respect output variable object type', async () => {
const scope = new Context({
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output(token, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('test')
})
it('should respect output variable string type', async () => {
const scope = new Context({
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output(token, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toEqual({ a: { b: 42 } })
})
})
})
-28
View File
@@ -1,28 +0,0 @@
import { isFunction } from '../util'
import { Hash } from './hash'
import { Tag, TagClass, TagRenderReturn } from './tag'
import { TagToken, TopLevelToken } from '../tokens'
import { Emitter } from '../emitters'
import { Context } from '../context'
import type { Liquid } from '../liquid'
export interface TagImplOptions {
[key: string]: any
parse?: (this: Tag & TagImplOptions, token: TagToken, remainingTokens: TopLevelToken[]) => void;
render: (this: Tag & TagImplOptions, ctx: Context, emitter: Emitter, hash: Record<string, any>) => TagRenderReturn;
}
export function createTagClass (options: TagImplOptions): TagClass {
return class extends Tag {
constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) {
super(token, tokens, liquid)
if (isFunction(options.parse)) {
options.parse.call(this, token, tokens)
}
}
* render (ctx: Context, emitter: Emitter): TagRenderReturn {
const hash = (yield new Hash(this.token.args, ctx.opts.keyValueSeparator).render(ctx)) as Record<string, any>
return yield options.render.call(this, ctx, emitter, hash)
}
}
}
+2 -4
View File
@@ -1,10 +1,9 @@
import { Drop } from '../drop/drop'
export const toString = Object.prototype.toString
export const hasOwnProperty = Object.prototype.hasOwnProperty
const toLowerCase = String.prototype.toLowerCase
export const hasOwnProperty = Object.hasOwnProperty
export function isString (value: any): value is string {
return typeof value === 'string'
}
@@ -90,8 +89,7 @@ export function isUndefined (value: any): boolean {
}
export function isArray (value: any): value is any[] {
// be compatible with IE 8
return toString.call(value) === '[object Array]'
return Array.isArray(value)
}
export function isArrayLike (value: any): value is any[] {
+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
@@ -1,39 +0,0 @@
import { Liquid } from '../../../src/liquid'
describe('LiquidOptions#*keepOutputType*', function () {
it('should respect keepOutputType', async function () {
const engine = new Liquid({
keepOutputType: true
})
const context = {
'my-boolean': true,
'my-number': 42,
'my-string': 'test'
}
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
expect(booleanHtml).toBe(true)
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
expect(numberHtml).toBe(42)
const html = await engine.parseAndRender('{{my-string}}', context)
expect(html).toBe('test')
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
expect(composedHtml).toBe('test:42')
})
it('should respect keepOutputType = false as default', async function () {
const engine = new Liquid()
const context = {
'my-boolean': true,
'my-number': 42,
'my-string': 'test'
}
const booleanHtml = await engine.parseAndRender('{{my-boolean}}', context)
expect(booleanHtml).toBe('true')
const numberHtml = await engine.parseAndRender('{{my-number}}', context)
expect(numberHtml).toBe('42')
const html = await engine.parseAndRender('{{my-string}}', context)
expect(html).toBe('test')
const composedHtml = await engine.parseAndRender('{{my-string}}:{{my-number}}', context)
expect(composedHtml).toBe('test:42')
})
})
+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 ''
}
}
+4 -3
View File
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "es6",
"module":"CommonJS",
"lib": ["es2015", "es2016", "es2017", "dom"],
"target": "ES2020",
"module":"ES2020",
"lib": ["ES2020", "dom"],
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"declaration": true,