diff --git a/CHANGELOG.md b/CHANGELOG.md index 29bc850119..c2a9db27bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [10.3.0](https://github.com/harttle/liquidjs/compare/v10.2.0...v10.3.0) (2022-12-11) + + +### Features + +* support disable outputEscape for specific filters, [#565](https://github.com/harttle/liquidjs/issues/565) ([e6db371](https://github.com/harttle/liquidjs/commit/e6db371519f0fb3b0068347cfb2016aed386c8fa)) + # [10.2.0](https://github.com/harttle/liquidjs/compare/v10.1.0...v10.2.0) (2022-12-02) diff --git a/docs/package-lock.json b/docs/package-lock.json index af6dacaaa5..b262bde54a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -978,9 +978,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -6041,9 +6041,9 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "deep-is": { "version": "0.1.3", diff --git a/docs/source/filters/date.md b/docs/source/filters/date.md index 91dfae69eb..f180e412bb 100644 --- a/docs/source/filters/date.md +++ b/docs/source/filters/date.md @@ -20,6 +20,20 @@ Fri, Jul 17, 15 Date will be converted to local time before output. To avoid that, you can set `timezoneOffset` LiquidJS option to `0`, its default value is your local timezone offset which can be obtained by `new Date().getTimezoneOffset()`. {% endnote %} +And you can set a timezone for each individual `date` filter via the second parameter: + +Input +```liquid +{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", 360}} // equivalent to setting `options.timezoneOffset` to `360`. +{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", "Asia/Colombo" }} +``` + +Output +```liquid +1990-12-31T17:00:00 +1991-01-01T04:30:00 +``` + Input ```liquid {{ article.published_at | date: "%Y" }} diff --git a/docs/source/zh-cn/filters/date.md b/docs/source/zh-cn/filters/date.md index 90505fafae..3c02c22e38 100644 --- a/docs/source/zh-cn/filters/date.md +++ b/docs/source/zh-cn/filters/date.md @@ -20,6 +20,21 @@ Fri, Jul 17, 15 日期在输出时会转换为当地时区,设置 `timezoneOffset` LiquidJS 参数可以指定一个不同的时区。或者设置 `preserveTimezones` 为 `true` 来保持字面量时间戳的时区,数据中的日期对象不受此参数的影响。 {% endnote %} +你也可以在使用 `date` 时再设置时区: + +输入 +```liquid +{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", 360}} // 等价于设置 `options.timezoneOffset` to `360`. +{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S", "Asia/Colombo" }} +``` + +输出 +```liquid +1990-12-31T17:00:00 +1991-01-01T04:30:00 +``` + + 输入 ```liquid {{ article.published_at | date: "%Y" }} @@ -30,7 +45,7 @@ Fri, Jul 17, 15 2015 ``` -对于包含格式正确的字符串也好使: +输入也可以是符合 JavaScript `Date` 格式的字符串:: 输入 ```liquid diff --git a/package-lock.json b/package-lock.json index 8d86b5ddec..75f544a439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "liquidjs", - "version": "10.2.0", + "version": "10.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 213867a86a..e74e4a9751 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "liquidjs", - "version": "10.2.0", + "version": "10.3.0", "description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.", "main": "dist/liquid.node.cjs.js", "module": "dist/liquid.node.esm.js", 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/liquid-options.ts b/src/liquid-options.ts index ce1cf6ad41..95143c5cda 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -3,7 +3,8 @@ import { LRU, LiquidCache } from './cache' import { FS } from './fs/fs' import * as fs from './fs/node' import { defaultOperators, Operators } from './render' -import { filters } from './filters' +import { json } from './filters/misc' +import { escape } from './filters/html' type OutputEscape = (value: any) => string type OutputEscapeOption = 'escape' | 'json' | OutputEscape @@ -182,15 +183,11 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions { return options as NormalizedFullOptions } -function getOutputEscapeFunction (nameOrFunction: OutputEscapeOption) { - if (isString(nameOrFunction)) { - const filterImpl = filters[nameOrFunction] - assert(isFunction(filterImpl), `filter "${nameOrFunction}" not found`) - return filterImpl - } else { - assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function') - return nameOrFunction - } +function getOutputEscapeFunction (nameOrFunction: OutputEscapeOption): OutputEscape { + if (nameOrFunction === 'escape') return escape + if (nameOrFunction === 'json') return json + assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function') + return nameOrFunction } export function normalizeDirectoryList (value: any): string[] { 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) + }) + }) })