Skip to content

Commit

Permalink
fix: corner case for concat filter without argument, #481
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Mar 2, 2022
1 parent 15b78eb commit aa95517
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 4 deletions.
25 changes: 24 additions & 1 deletion docs/source/tutorials/differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@
title: Differences with Shopify/liquid
---

All filters and tags in [shopify/liquid](https://github.com/Shopify/liquid) are supposed to be built in LiquidJS, but not those business-logic specific tags/filters which are typically from Shopify themes (see [Plugins List][plugins] in case you're looking for them and feel free to add yours to the list). Though being compatible the Ruby version is one of our priorities, there are still some differences:
## Compatibility

Being compatible the Ruby version is one of our priorities. Liquid language is originally [implemented in Ruby][ruby-liquid] and used by Shopify, Jekyll and thus Github Pages, as you can see it's one of the most popular template engines in Ruby. There're lots of people using LiquidJS to serve their templates originally written for Shopify themes and Jekyll sites.

So "being compatible" means serve developers from Shopify and Jekyll well:

- **Well-formed Liquid template should work just fine in LiquidJS**. For example, `forloop.index` should be 1-indexed, `nil` should be rendered as empty string rather than `undefined`, etc. Although some features (e.g. [#236][#236]) are not feasible in JavaScript, at least we're trying to implement all the semantics of Liquid language.
- **All filters and tags in [shopify/liquid][ruby-liquid] are supposed to be built in LiquidJS**. But not those business-logic specific tags/filters typically defined by Shopify platform. Those features should be maintained as [plugins][plugins]. For filters/tags that are not business-logic specific, like `{% layout %}`, and extremely useful, feel free to file an issue.

In the meantime, it's now implemented in JavaScript, that means it has to be more powerful:

* **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`.

## Differences

Though we're trying to be compatible with the Ruby version, there are still some differences:

* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26][#26].
* Number. In JavaScript we cannot distinguish or convert between `float` and `integer`, see [#59][#59]. And when applied `size` filter, numbers always return 0, which is 8 for integer in ruby, cause they do not have a `length` property.
Expand All @@ -20,6 +38,11 @@ All filters and tags in [shopify/liquid](https://github.com/Shopify/liquid) are
[#59]: https://github.com/harttle/liquidjs/issues/59
[#208]: https://github.com/harttle/liquidjs/issues/208
[#212]: https://github.com/harttle/liquidjs/issues/212
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#Plugin-List
[ruby-liquid]: https://github.com/Shopify/liquid
[afs]: https://liquidjs.com/tutorials/render-file.html#Abstract-File-System
25 changes: 24 additions & 1 deletion docs/source/zh-cn/tutorials/differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@
title: 和 Shopify/liquid 的区别
---

[Shopify/liquid](https://github.com/Shopify/liquid) 中的所有标签和过滤器 LiquidJS 都支持,但不包括 Shopify 主题中业务逻辑相关的标签和过滤器(如果你在找这些标签可以参考 [插件列表][plugins],也欢迎把你的插件添加到列表中)。尽管原则上我们尽力兼容于 Shopify/liquid,但仍然存在一些区别:
## 兼容性

LiquidJS 一直很重视兼容于 Ruby 版本的 Liquid。Liquid 模板语言最初由 [Ruby 实现][ruby-liquid],用于 Shopify,Jekyll 以及 Github Pages,它是 Ruby 里最流行的模板引擎之一。因此由很多人用 LiquidJS 来渲染他们的 Shopify 主题和 Jekyll 站点。

所以“兼容”意味着让这些开发者有很好的使用体验:

- **LiquidJS 应当能够渲染语法正确的 Liquid 模板**。例如 `forloop.index` 应该是 1 开始的下表,`nil` 应该渲染成空字符串而不是 `undefined` 等。即使有些功能(例如 [#236][#236])用 JavaScript 很难实现,但至少 LiquidJS 会尝试实现所有的 Liquid 语义。
- **所有 [shopify/liquid][ruby-liquid] 里的标签和过滤器 LiquidJS 都要实现**。这之外有些业务逻辑相关的标签/过滤器尤其是 Shopify 平台上的那些,应该维护在 [插件][plugins] 里。但是这其中有一些很有用的标签(比如 `{% layout %}`)LiquidJS 也会考虑实现,可以去开个 Issue 讨论一下。

同时,既然现在用 JavaScript 实现了,那 Liquid 应该有更强的功能:

* **完全支持异步**。所有过滤器和标签都可以实现为异步,只需要返回 `Promise` 即可。
* **同时支持同步**。对一些非 I/O 密集的场景,同步渲染会更快。只要模板包含的标签和过滤器都支持同步,你就可以调用类似 `.renderSync()` 这样的 API。所有内置标签和过滤器都同时支持同步和异步。
* **[抽象文件系统][afs]**。和异步功能一起使用,LiquidJS 可以实现渲染数据库里的模板 [#414][#414],远程 HTTP 服务器上的模板 [#485][#485],等等。
* **额外的标签和过滤器**。比如 `layout``json`

## 区别

[Shopify/liquid][ruby-liquid] 中的所有标签和过滤器 LiquidJS 都支持,但不包括 Shopify 主题中业务逻辑相关的标签和过滤器(如果你在找这些标签可以参考 [插件列表][plugins],也欢迎把你的插件添加到列表中)。尽管原则上我们尽力兼容于 Shopify/liquid,但仍然存在一些区别:

* 真和假。在 LiquidJS 中 `undefined`, `null`, `false` 是假,之外的都是真;在 Ruby 中 `nil``false` 是假,其他都是真。见 [#26][#26]
* 数字。JavaScript 不区分浮点数和整数,因此缺失一部分整数算术,见 [#59][#59]。此外 `size` 过滤器作用于数字时总是返回零,而不是 Ruby 中的浮点数或整数的内存大小。
Expand All @@ -20,6 +38,11 @@ title: 和 Shopify/liquid 的区别
[#59]: https://github.com/harttle/liquidjs/issues/59
[#208]: https://github.com/harttle/liquidjs/issues/208
[#212]: https://github.com/harttle/liquidjs/issues/212
[#236]: https://github.com/harttle/liquidjs/issues/236
[#414]: https://github.com/harttle/liquidjs/discussions/414
[#485]: https://github.com/harttle/liquidjs/discussions/485
[sort]: https://liquidjs.com/filters/sort.html
[stable-sort]: https://v8.dev/features/stable-sort
[plugins]: ./plugins.html#插件列表
[ruby-liquid]: https://github.com/Shopify/liquid
[afs]: https://liquidjs.com/tutorials/render-file.html#Abstract-File-System
3 changes: 2 additions & 1 deletion src/builtin/filters/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export function compact<T> (this: FilterImpl, arr: T[]) {
return toArray(arr).filter(x => !isNil(toValue(x)))
}

export function concat<T1, T2> (v: T1[], arg: T2[]): (T1 | T2)[] {
export function concat<T1, T2> (v: T1[], arg: T2[] = []): (T1 | T2)[] {
v = toValue(v)
arg = toArray(arg).map(v => toValue(v))
return toArray(v).concat(arg)
}

Expand Down
8 changes: 7 additions & 1 deletion test/e2e/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,14 @@ describe('Issues', function () {
{{ foo | newline_to_br }}
{{ foo | strip_html }}
{{ foo | truncatewords }}
{{ foo | concat | json }}
`)
const html = await engine.render(tpl, { foo: undefined })
expect(html.trim()).to.equal('')
expect(html.trim()).to.equal('[]')
})
it('#481 concat should always return an array', async () => {
const engine = new Liquid()
const html = await engine.parseAndRender(`{{ foo | concat | json }}`)
expect(html).to.equal('[]')
})
})
13 changes: 13 additions & 0 deletions test/integration/builtin/filters/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,24 @@ describe('filters/array', function () {
})

describe('concat', () => {
it('should concat args value', async () => {
const scope = { val: ['hey'], arr: ['foo', 'bar'] }
await test('{{ val | concat: arr | join: "," }}', scope, 'hey,foo,bar')
})
it('should support undefined left value', async () => {
const scope = { arr: ['foo', 'bar'] }
await test('{{ notDefined | concat: arr | join: "," }}', scope, 'foo,bar')
})
it('should ignore nil left value', async () => {
const scope = { undefinedValue: undefined, nullValue: null, arr: ['foo', 'bar'] }
await test('{{ undefinedValue | concat: arr | join: "," }}', scope, 'foo,bar')
await test('{{ nullValue | concat: arr | join: "," }}', scope, 'foo,bar')
})
it('should ignore nil right value', async () => {
const scope = { nullValue: null }
await test('{{ nullValue | concat | join: "," }}', scope, '')
await test('{{ nullValue | concat: nil | join: "," }}', scope, '')
})
})

describe('reverse', function () {
Expand Down

0 comments on commit aa95517

Please sign in to comment.