From 842b45c96a46290a1e1ba43fc5cc7a465f4ba9de Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Mon, 6 May 2024 20:33:34 +0800 Subject: [PATCH] feat: jsonify, inspect, to_integer, normalize_whitespace filters --- docs/source/_data/sidebar.yml | 4 ++ docs/source/filters/inspect.md | 42 +++++++++++++++++++ docs/source/filters/jsonify.md | 9 ++++ docs/source/filters/normalize_whitespace.md | 17 ++++++++ docs/source/filters/overview.md | 4 +- docs/source/filters/to_integer.md | 17 ++++++++ docs/source/tutorials/differences.md | 4 +- docs/source/zh-cn/filters/inspect.md | 42 +++++++++++++++++++ docs/source/zh-cn/filters/json.md | 4 +- docs/source/zh-cn/filters/jsonify.md | 9 ++++ .../zh-cn/filters/normalize_whitespace.md | 17 ++++++++ docs/source/zh-cn/filters/overview.md | 4 +- docs/source/zh-cn/filters/to_integer.md | 17 ++++++++ src/filters/index.ts | 6 +-- src/filters/misc.ts | 31 ++++++++++++-- src/filters/string.ts | 5 +++ src/liquid-options.ts | 4 +- test/integration/filters/misc.spec.ts | 29 +++++++++++++ test/integration/filters/string.spec.ts | 10 +++++ 19 files changed, 258 insertions(+), 17 deletions(-) create mode 100644 docs/source/filters/inspect.md create mode 100644 docs/source/filters/jsonify.md create mode 100644 docs/source/filters/normalize_whitespace.md create mode 100644 docs/source/filters/to_integer.md create mode 100644 docs/source/zh-cn/filters/inspect.md create mode 100644 docs/source/zh-cn/filters/jsonify.md create mode 100644 docs/source/zh-cn/filters/normalize_whitespace.md create mode 100644 docs/source/zh-cn/filters/to_integer.md diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index c609bb196a..ac0631b11b 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -47,14 +47,17 @@ filters: floor: floor.html group_by: group_by.html group_by_exp: group_by_exp.html + inspect: inspect.html join: join.html json: json.html + jsonify: jsonify.html last: last.html lstrip: lstrip.html map: map.html minus: minus.html modulo: modulo.html newline_to_br: newline_to_br.html + normalize_whitespace: normalize_whitespace.html plus: plus.html pop: pop.html push: push.html @@ -80,6 +83,7 @@ filters: strip_newlines: strip_newlines.html sum: sum.html times: times.html + to_integer: to_integer.html truncate: truncate.html truncatewords: truncatewords.html uniq: uniq.html diff --git a/docs/source/filters/inspect.md b/docs/source/filters/inspect.md new file mode 100644 index 0000000000..7abc85619f --- /dev/null +++ b/docs/source/filters/inspect.md @@ -0,0 +1,42 @@ +--- +title: inspect +--- + +{% since %}v10.13.0{% endsince %} + +Similar with `json`, but `inspect` allows cyclic structure. For the scope below: + +``` +const foo = { + bar: 'BAR' +} +foo.foo = foo +const scope = { foo } +``` + +Input +```liquid +{% foo | inspect %} +``` + +Output +```text +{"bar":"BAR","foo":"[Circular]"} +``` + +## Formatting + +An additional `space` argument can be specified for the indent width. + +Input +```liquid +{{ foo | inspect: 4 }} +``` + +Output +```text +{ + "bar": "BAR", + "foo": "[Circular]" +} +``` diff --git a/docs/source/filters/jsonify.md b/docs/source/filters/jsonify.md new file mode 100644 index 0000000000..dd2ca819dd --- /dev/null +++ b/docs/source/filters/jsonify.md @@ -0,0 +1,9 @@ +--- +title: jsonify +--- + +{% since %}v10.13.0{% endsince %} + +See [json][json]. + +[json]: /filters/json.html diff --git a/docs/source/filters/normalize_whitespace.md b/docs/source/filters/normalize_whitespace.md new file mode 100644 index 0000000000..bafc290b2a --- /dev/null +++ b/docs/source/filters/normalize_whitespace.md @@ -0,0 +1,17 @@ +--- +title: normalize_whitespace +--- + +{% since %}v10.13.0{% endsince %} + +Replace any occurrence of whitespace with a single space. + +Input +```liquid +{{ "a \n b" | normalize_whitespace }} +``` + +Output +```html +a b +``` diff --git a/docs/source/filters/overview.md b/docs/source/filters/overview.md index 03559308b6..a5e11d3b8e 100644 --- a/docs/source/filters/overview.md +++ b/docs/source/filters/overview.md @@ -10,10 +10,10 @@ There's 40+ filters supported by LiquidJS. These filters can be categorized into Categories | Filters --- | --- Math | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most -String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last,remove, remove_first, remove_last, truncate, truncatewords +String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last,remove, remove_first, remove_last, truncate, truncatewords, normalize_whitespace HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br Array | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift Date | date -Misc | default, json, raw +Misc | default, json, jsonify, inspect, raw, to_integer [shopify/liquid]: https://github.com/Shopify/liquid diff --git a/docs/source/filters/to_integer.md b/docs/source/filters/to_integer.md new file mode 100644 index 0000000000..d635f85566 --- /dev/null +++ b/docs/source/filters/to_integer.md @@ -0,0 +1,17 @@ +--- +title: to_integer +--- + +{% since %}v10.13.0{% endsince %} + +Convert values to number. + +Input +```liquid +{{ "123" | to_integer | json }} +``` + +Output +```text +123 +``` diff --git a/docs/source/tutorials/differences.md b/docs/source/tutorials/differences.md index 91a6b43dd4..e6bb6cfcbd 100644 --- a/docs/source/tutorials/differences.md +++ b/docs/source/tutorials/differences.md @@ -16,7 +16,7 @@ In the meantime, it's now implemented in JavaScript, that means it has to be mor * **Async as first-class citizen**. Filters and tags can be implemented asynchronously by return a `Promise`. * **Also can be sync**. For scenarios that are not I/O intensive, render synchronously can be much faster. You can call synchronous APIs like `.renderSync()` as long as all the filters and tags in template support to be rendered synchronously. All builtin filters/tags support both sync and async render. * **[Abstract file system][afs]**. Along with async feature, LiquidJS can be used to serve templates stored in Databases [#414][#414], on remote HTTP server [#485][#485], and so on. -* **Additional tags and filters** like `layout` and `json`, see below for details. +* **Additional tags and filters** like `layout` and `json`, `inspect`, `where_exp`, `group_by`, etc., see below for details. ## Differences @@ -31,7 +31,7 @@ Though we're trying to be compatible with the Ruby version, there are still some * Trailing unmatched characters inside filters are allowed in shopify/liquid but not in LiquidJS. It means filter arguments without a colon like `{%raw%}{{ "a b" | split " "}}{%endraw%}` will throw an error in LiquidJS. This is intended to improve Liquid usability, see [#208][#208] and [#212][#212]. * LiquidJS has more tags/filters than [the Liquid language][liquid]: * LiquidJS-defined tags: [layout][layout], [render][render] and corresponding `block` tag. - * LiquidJS-defined filters: [json][json]. + * LiquidJS-defined filters: [json][json], group_by, group_by_exp, where_exp, jsonify, inspect, etc. * Tags/filters that don't depend on Shopify platform are borrowed from [Shopify][shopify-tags]. * Tags/filters that don't depend on Jekyll framework are borrowed from [Jekyll][jekyll-filters]. * Some tags/filters behave differently: [date][date] filter. diff --git a/docs/source/zh-cn/filters/inspect.md b/docs/source/zh-cn/filters/inspect.md new file mode 100644 index 0000000000..e59dff12fe --- /dev/null +++ b/docs/source/zh-cn/filters/inspect.md @@ -0,0 +1,42 @@ +--- +title: inspect +--- + +{% since %}v10.13.0{% endsince %} + +类似于 `json`,但可以处理循环引用的情况。例如对于上下文: + +``` +const foo = { + bar: 'BAR' +} +foo.foo = foo +const scope = { foo } +``` + +输入 +```liquid +{% foo | inspect %} +``` + +输出 +```text +{"bar":"BAR","foo":"[Circular]"} +``` + +## 格式化 + +可以指定一个 `space` 参数来缩进长度。 + +输入 +```liquid +{{ foo | inspect: 4 }} +``` + +输出 +```text +{ + "bar": "BAR", + "foo": "[Circular]" +} +``` diff --git a/docs/source/zh-cn/filters/json.md b/docs/source/zh-cn/filters/json.md index 14f1647334..3fc6e71f75 100644 --- a/docs/source/zh-cn/filters/json.md +++ b/docs/source/zh-cn/filters/json.md @@ -23,13 +23,13 @@ title: json 可以指定一个 `space` 参数来格式化 JSON。 -Input +输入 ```liquid {% assign arr = "foo bar coo" | split: " " %} {{ arr | json: 4 }} ``` -Output +输出 ```text [ "foo", diff --git a/docs/source/zh-cn/filters/jsonify.md b/docs/source/zh-cn/filters/jsonify.md new file mode 100644 index 0000000000..55ff26ed69 --- /dev/null +++ b/docs/source/zh-cn/filters/jsonify.md @@ -0,0 +1,9 @@ +--- +title: jsonify +--- + +{% since %}v10.13.0{% endsince %} + +见 [json][json]。 + +[json]: ./json.html diff --git a/docs/source/zh-cn/filters/normalize_whitespace.md b/docs/source/zh-cn/filters/normalize_whitespace.md new file mode 100644 index 0000000000..db203419b6 --- /dev/null +++ b/docs/source/zh-cn/filters/normalize_whitespace.md @@ -0,0 +1,17 @@ +--- +title: normalize_whitespace +--- + +{% since %}v10.13.0{% endsince %} + +把连续的空白字符替换为单个空格。 + +输入 +```liquid +{{ "a \n b" | normalize_whitespace }} +``` + +输出 +```html +a b +``` diff --git a/docs/source/zh-cn/filters/overview.md b/docs/source/zh-cn/filters/overview.md index 8effee0070..ce13a9e598 100644 --- a/docs/source/zh-cn/filters/overview.md +++ b/docs/source/zh-cn/filters/overview.md @@ -10,10 +10,10 @@ LiquidJS 共支持 40+ 个过滤器,可以分为如下几类: 类别 | 过滤器 --- | --- 数学 | plus, minus, modulo, times, floor, ceil, round, divided_by, abs, at_least, at_most -字符串 | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last, remove, remove_first, remove_last, truncate, truncatewords +字符串 | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, strip_newlines, split, replace, replace_first, replace_last, remove, remove_first, remove_last, truncate, truncatewords, normalize_whitespace HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br 数组 | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_by_exp, find, find_exp, first, last, join, reverse, concat, compact, size, push, pop, shift, unshift 日期 | date -其他 | default, json +其他 | default, json, jsonify, inspect, raw, to_integer [shopify/liquid]: https://github.com/Shopify/liquid diff --git a/docs/source/zh-cn/filters/to_integer.md b/docs/source/zh-cn/filters/to_integer.md new file mode 100644 index 0000000000..e79c879ecf --- /dev/null +++ b/docs/source/zh-cn/filters/to_integer.md @@ -0,0 +1,17 @@ +--- +title: to_integer +--- + +{% since %}v10.13.0{% endsince %} + +转换为数字类型。 + +输入 +```liquid +{{ "123" | to_integer | json }} +``` + +输出 +```text +123 +``` diff --git a/src/filters/index.ts b/src/filters/index.ts index 1f296c7262..caf040c9bc 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, raw } from './misc' +import misc from './misc' import { FilterImplOptions } from '../template' export const filters: Record = { @@ -14,7 +14,5 @@ export const filters: Record = { ...arrayFilters, ...dateFilters, ...stringFilters, - json, - raw, - default: Default + ...misc } diff --git a/src/filters/misc.ts b/src/filters/misc.ts index 806c58ea1a..57ed578121 100644 --- a/src/filters/misc.ts +++ b/src/filters/misc.ts @@ -2,18 +2,43 @@ import { isFalsy } from '../render/boolean' import { identify, isArray, isString, toValue } from '../util/underscore' import { FilterImpl } from '../template' -export function Default (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 { +function defaultFilter (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 { value = toValue(value) if (isArray(value) || isString(value)) return value.length ? value : defaultValue if (value === false && (new Map(args)).get('allow_false')) return false as T1 return isFalsy(value, this.context) ? defaultValue : value } -export function json (value: any, space = 0) { +function json (value: any, space = 0) { return JSON.stringify(value, null, space) } -export const raw = { +function inspect (value: any, space = 0) { + const ancestors: object[] = [] + return JSON.stringify(value, function (this: unknown, _key: unknown, value: any) { + if (typeof value !== 'object' || value === null) return value + // `this` is the object that value is contained in, i.e., its direct parent. + while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) ancestors.pop() + if (ancestors.includes(value)) return '[Circular]' + ancestors.push(value) + return value + }, space) +} + +function to_integer (value: any) { + return Number(value) +} + +const raw = { raw: true, handler: identify } + +export default { + default: defaultFilter, + raw, + jsonify: json, + to_integer, + json, + inspect +} diff --git a/src/filters/string.ts b/src/filters/string.ts index 2a10135818..1bdc315d81 100644 --- a/src/filters/string.ts +++ b/src/filters/string.ts @@ -112,3 +112,8 @@ export function truncatewords (v: string, words = 15, o = '...') { if (arr.length >= words) ret += o return ret } + +export function normalize_whitespace (v: string) { + v = stringify(v) + return v.replace(/\s+/g, ' ') +} diff --git a/src/liquid-options.ts b/src/liquid-options.ts index c220702995..89223bbf92 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -3,7 +3,7 @@ import { LRU, LiquidCache } from './cache' import { FS, LookupType } from './fs' import * as fs from './fs/fs-impl' import { defaultOperators, Operators } from './render' -import { json } from './filters/misc' +import misc from './filters/misc' import { escape } from './filters/html' type OutputEscape = (value: any) => string @@ -193,7 +193,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions { function getOutputEscapeFunction (nameOrFunction: OutputEscapeOption): OutputEscape { if (nameOrFunction === 'escape') return escape - if (nameOrFunction === 'json') return json + if (nameOrFunction === 'json') return misc.json assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function') return nameOrFunction } diff --git a/test/integration/filters/misc.spec.ts b/test/integration/filters/misc.spec.ts index bc6f92117d..f60b260156 100644 --- a/test/integration/filters/misc.spec.ts +++ b/test/integration/filters/misc.spec.ts @@ -30,4 +30,33 @@ describe('filters/object', function () { expect(liquid.parseAndRenderSync('{{obj | json: 4}}', scope)).toBe(result) }) }) + describe('jsonify', function () { + it('should stringify string', async () => expect(await liquid.parseAndRender('{{"foo" | jsonify}}')).toBe('"foo"')) + }) + describe('inspect', function () { + it('should inspect string', async () => expect(await liquid.parseAndRender('{{"foo" | inspect}}')).toBe('"foo"')) + it('should inspect object', () => { + const text = '{{foo | inspect}}' + const foo = { bar: 'bar' } + const expected = '{"bar":"bar"}' + return expect(liquid.parseAndRenderSync(text, { foo })).toBe(expected) + }) + it('should inspect cyclic object', () => { + const text = '{{foo | inspect}}' + const foo: any = { bar: 'bar' } + foo.foo = foo + const expected = '{"bar":"bar","foo":"[Circular]"}' + return expect(liquid.parseAndRenderSync(text, { foo })).toBe(expected) + }) + it('should support space argument', () => { + const text = '{{foo | inspect: 4}}' + const foo: any = { bar: 'bar' } + foo.foo = foo + const expected = '{\n "bar": "bar",\n "foo": "[Circular]"\n}' + return expect(liquid.parseAndRenderSync(text, { foo })).toBe(expected) + }) + }) + describe('to_integer', function () { + it('should stringify string', () => expect(liquid.parseAndRenderSync('{{ "123" | to_integer | json }}')).toBe('123')) + }) }) diff --git a/test/integration/filters/string.spec.ts b/test/integration/filters/string.spec.ts index bc54e07ce6..3cb494da5a 100644 --- a/test/integration/filters/string.spec.ts +++ b/test/integration/filters/string.spec.ts @@ -1,6 +1,8 @@ import { test } from '../../stub/render' +import { Liquid } from '../../../src/liquid' describe('filters/string', function () { + const liquid = new Liquid() describe('append', function () { it('should return "-3abc" for -3, "abc"', () => test('{{ -3 | append: "abc" }}', '-3abc')) @@ -228,4 +230,12 @@ describe('filters/string', function () { 'Take my protein pills and put my helmet on') }) }) + describe('normalize_whitespace', () => { + it('should replace " \n " with " "', () => { + expect(liquid.parseAndRenderSync('{{ "a \n b" | normalize_whitespace }}')).toEqual('a b') + }) + it('should replace multiple occurrences', () => { + expect(liquid.parseAndRenderSync('{{ "a \n b c" | normalize_whitespace }}')).toEqual('a b c') + }) + }) })