feat: jsonify, inspect, to_integer, normalize_whitespace filters

This commit is contained in:
Yang Jun
2024-05-06 21:10:27 +08:00
committed by Jun Yang
parent ea0eab93d1
commit 842b45c96a
19 changed files with 258 additions and 17 deletions
+4
View File
@@ -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
+42
View File
@@ -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]"
}
```
+9
View File
@@ -0,0 +1,9 @@
---
title: jsonify
---
{% since %}v10.13.0{% endsince %}
See [json][json].
[json]: /filters/json.html
@@ -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
```
+2 -2
View File
@@ -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
+17
View File
@@ -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
```
+2 -2
View File
@@ -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.
+42
View File
@@ -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]"
}
```
+2 -2
View File
@@ -23,13 +23,13 @@ title: json
可以指定一个 `space` 参数来格式化 JSON。
Input
输入
```liquid
{% assign arr = "foo bar coo" | split: " " %}
{{ arr | json: 4 }}
```
Output
输出
```text
[
"foo",
+9
View File
@@ -0,0 +1,9 @@
---
title: jsonify
---
{% since %}v10.13.0{% endsince %}
见 [json][json]。
[json]: ./json.html
@@ -0,0 +1,17 @@
---
title: normalize_whitespace
---
{% since %}v10.13.0{% endsince %}
把连续的空白字符替换为单个空格。
输入
```liquid
{{ "a \n b" | normalize_whitespace }}
```
输出
```html
a b
```
+2 -2
View File
@@ -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
+17
View File
@@ -0,0 +1,17 @@
---
title: to_integer
---
{% since %}v10.13.0{% endsince %}
转换为数字类型。
输入
```liquid
{{ "123" | to_integer | json }}
```
输出
```text
123
```
+2 -4
View File
@@ -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<string, FilterImplOptions> = {
@@ -14,7 +14,5 @@ export const filters: Record<string, FilterImplOptions> = {
...arrayFilters,
...dateFilters,
...stringFilters,
json,
raw,
default: Default
...misc
}
+28 -3
View File
@@ -2,18 +2,43 @@ import { isFalsy } from '../render/boolean'
import { identify, isArray, isString, toValue } from '../util/underscore'
import { FilterImpl } from '../template'
export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
function defaultFilter<T1 extends boolean, T2> (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
}
+5
View File
@@ -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, ' ')
}
+2 -2
View File
@@ -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
}
+29
View File
@@ -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'))
})
})
+10
View File
@@ -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')
})
})
})