From e6db371519f0fb3b0068347cfb2016aed386c8fa Mon Sep 17 00:00:00 2001 From: Jun Yang Date: Mon, 12 Dec 2022 02:17:18 +0800 Subject: [PATCH] feat: support disable outputEscape for specific filters, #565 --- src/filters/index.ts | 3 +- src/filters/misc.ts | 5 ++- src/template/filter-impl-options.ts | 9 +++-- src/template/filter.ts | 16 +++++---- src/template/output.ts | 4 +-- test/integration/liquid/register-filters.ts | 40 +++++++++++++++++---- 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/filters/index.ts b/src/filters/index.ts index 7347035265..1f296c7262 100644 --- a/src/filters/index.ts +++ b/src/filters/index.ts @@ -4,7 +4,7 @@ import * as urlFilters from './url' import * as arrayFilters from './array' import * as dateFilters from './date' import * as stringFilters from './string' -import { Default, json } from './misc' +import { Default, json, raw } from './misc' import { FilterImplOptions } from '../template' export const filters: Record = { @@ -15,5 +15,6 @@ export const filters: Record = { ...dateFilters, ...stringFilters, json, + raw, default: Default } diff --git a/src/filters/misc.ts b/src/filters/misc.ts index 71968bbbe4..ca4421be9b 100644 --- a/src/filters/misc.ts +++ b/src/filters/misc.ts @@ -13,4 +13,7 @@ export function json (value: any) { return JSON.stringify(value) } -export const raw = identify +export const raw = { + raw: true, + handler: identify +} diff --git a/src/template/filter-impl-options.ts b/src/template/filter-impl-options.ts index 94c8e37c2c..d94d7b591a 100644 --- a/src/template/filter-impl-options.ts +++ b/src/template/filter-impl-options.ts @@ -6,6 +6,11 @@ export interface FilterImpl { liquid: Liquid; } -export interface FilterImplOptions { - (this: FilterImpl, value: any, ...args: any[]): any; +export type FilterHandler = (this: FilterImpl, value: any, ...args: any[]) => any; + +export interface FilterOptions { + handler: FilterHandler; + raw: boolean; } + +export type FilterImplOptions = FilterHandler | FilterOptions diff --git a/src/template/filter.ts b/src/template/filter.ts index 50f630616b..6ab4b0e815 100644 --- a/src/template/filter.ts +++ b/src/template/filter.ts @@ -1,19 +1,23 @@ import { evalToken } from '../render' import { Context } from '../context' -import { identify } from '../util/underscore' -import { FilterImplOptions } from './filter-impl-options' +import { identify, isFunction } from '../util/underscore' +import { FilterHandler, FilterImplOptions } from './filter-impl-options' import { FilterArg, isKeyValuePair } from '../parser/filter-arg' import { Liquid } from '../liquid' export class Filter { public name: string public args: FilterArg[] - private impl: FilterImplOptions + public readonly raw: boolean + private handler: FilterHandler private liquid: Liquid - public constructor (name: string, impl: FilterImplOptions | undefined, args: FilterArg[], liquid: Liquid) { + public constructor (name: string, options: FilterImplOptions | undefined, args: FilterArg[], liquid: Liquid) { this.name = name - this.impl = impl || identify + this.handler = isFunction(options) + ? options + : (isFunction(options?.handler) ? options!.handler : identify) + this.raw = !isFunction(options) && !!options?.raw this.args = args this.liquid = liquid } @@ -23,6 +27,6 @@ export class Filter { if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)]) else argv.push(yield evalToken(arg, context)) } - return this.impl.apply({ context, liquid: this.liquid }, [value, ...argv]) + return this.handler.apply({ context, liquid: this.liquid }, [value, ...argv]) } } diff --git a/src/template/output.ts b/src/template/output.ts index f97313eb98..6df047e76e 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -13,9 +13,7 @@ export class Output extends TemplateImpl implements Template { this.value = new Value(token.content, liquid) const filters = this.value.filters const outputEscape = liquid.options.outputEscape - if (filters.length && filters[filters.length - 1].name === 'raw') { - filters.pop() - } else if (outputEscape) { + if (!filters[filters.length - 1]?.raw && outputEscape) { filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid)) } } diff --git a/test/integration/liquid/register-filters.ts b/test/integration/liquid/register-filters.ts index 5d442df6c6..f2dd0fc47f 100644 --- a/test/integration/liquid/register-filters.ts +++ b/test/integration/liquid/register-filters.ts @@ -2,11 +2,14 @@ import { expect } from 'chai' import { Liquid } from '../../../src/liquid' describe('liquid#registerFilter()', function () { - const liquid = new Liquid() + let liquid: Liquid + beforeEach(() => { liquid = new Liquid() }) describe('key-value arguments', function () { - liquid.registerFilter('obj_test', function (...args) { - return JSON.stringify(args) + beforeEach(() => { + liquid.registerFilter('obj_test', function (...args) { + return JSON.stringify(args) + }) }) it('should support key-value arguments', async () => { const src = `{{ "a" | obj_test: k1: "v1", k2: foo }}` @@ -23,14 +26,39 @@ describe('liquid#registerFilter()', function () { }) describe('async filters', () => { - liquid.registerFilter('get_user_data', function (userId) { - return Promise.resolve({ userId, userName: userId.toUpperCase() }) - }) it('should support async filter', async () => { + liquid.registerFilter('get_user_data', function (userId) { + return Promise.resolve({ userId, userName: userId.toUpperCase() }) + }) const src = `{{ userId | get_user_data | json }}` const dst = '{"userId":"alice","userName":"ALICE"}' const html = await liquid.parseAndRender(src, { userId: 'alice' }) return expect(html).to.equal(dst) }) }) + + describe('raw filters', () => { + beforeEach(() => { + liquid = new Liquid({ + outputEscape: 'escape' + }) + }) + it('should escape filter output when outputEscape set to true', async () => { + liquid.registerFilter('break', (str) => str.replace(/\n/g, '
')) + const src = `{{ "a\nb" | break }}` + const dst = 'a<br/>b' + const html = await liquid.parseAndRender(src) + return expect(html).to.equal(dst) + }) + it('should not escape filter output when registered as "raw"', async () => { + liquid.registerFilter('break', { + handler: (str) => str.replace(/\n/g, '
'), + raw: true + }) + const src = `{{ "a\nb" | break }}` + const dst = 'a
b' + const html = await liquid.parseAndRender(src) + return expect(html).to.equal(dst) + }) + }) })