Repository: harttle/liquidjs
Pull request #693 (2024-05-12 07:00:19)

feat: escape filters from Jekyll


Patch:
diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml
index b44bef785..bf6affcb9 100644
--- a/docs/source/_data/sidebar.yml
+++ b/docs/source/_data/sidebar.yml
@@ -33,6 +33,7 @@ filters:
   at_most: at_most.html
   capitalize: capitalize.html
   ceil: ceil.html
+  cgi_escape: cgi_escape.html
   compact: compact.html
   concat: concat.html
   date: date.html
@@ -93,10 +94,12 @@ filters:
   uniq: uniq.html
   unshift: unshift.html
   upcase: upcase.html
+  uri_escape: uri_escape.html
   url_decode: url_decode.html
   url_encode: url_encode.html
   where: where.html
   where_exp: where_exp.html
+  xml_escape: xml_escape.html
 
 tags:
   overview: overview.html
diff --git a/docs/source/filters/cgi_escape.md b/docs/source/filters/cgi_escape.md
new file mode 100644
index 000000000..2544b841e
--- /dev/null
+++ b/docs/source/filters/cgi_escape.md
@@ -0,0 +1,17 @@
+---
+title: cgi_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+CGI escape a string for use in a URL. Replaces any special characters with appropriate `%XX` replacements. CGI escape normally replaces a space with a plus `+` sign.
+
+Input
+```liquid
+{{ "foo, bar; baz?" | cgi_escape }}
+```
+
+Output
+```text
+foo%2C+bar%3B+baz%3F
+```
diff --git a/docs/source/filters/overview.md b/docs/source/filters/overview.md
index ded8bdb55..c531cd281 100644
--- a/docs/source/filters/overview.md
+++ b/docs/source/filters/overview.md
@@ -11,7 +11,7 @@ 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, normalize_whitespace
-HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
+HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape
 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, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
 Misc | default, json, jsonify, inspect, raw, to_integer
diff --git a/docs/source/filters/uri_escape.md b/docs/source/filters/uri_escape.md
new file mode 100644
index 000000000..dbfd79a21
--- /dev/null
+++ b/docs/source/filters/uri_escape.md
@@ -0,0 +1,19 @@
+---
+title: uri_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+Percent encodes any special characters in a URI. URI escape normally replaces a space with `%20`. [Reserved characters][reserved] will not be escaped.
+
+Input
+```liquid
+{{ "http://foo.com/?q=foo, \bar?" | uri_escape }}
+```
+
+Output
+```text
+http://foo.com/?q=foo,%20%5Cbar?
+```
+
+[reserved]: https://en.wikipedia.org/wiki/Percent-encoding#Types_of_URI_characters
diff --git a/docs/source/filters/xml_escape.md b/docs/source/filters/xml_escape.md
new file mode 100644
index 000000000..33f5f61a6
--- /dev/null
+++ b/docs/source/filters/xml_escape.md
@@ -0,0 +1,17 @@
+---
+title: xml_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+Escape some text for use in XML.
+
+Input
+```liquid
+{{ "Have you read \'James & the Giant Peach\'?" | xml_escape }}
+```
+
+Output
+```text
+Have you read &#39;James &amp; the Giant Peach&#39;?
+```
diff --git a/docs/source/zh-cn/filters/cgi_escape.md b/docs/source/zh-cn/filters/cgi_escape.md
new file mode 100644
index 000000000..159dd5bdb
--- /dev/null
+++ b/docs/source/zh-cn/filters/cgi_escape.md
@@ -0,0 +1,17 @@
+---
+title: cgi_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+把字符串 CGI 转义，用于 URL。用对应的 `%XX` 替换特殊字符，空格会被转义为 `+` 号。
+
+输入
+```liquid
+{{ "foo, bar; baz?" | cgi_escape }}
+```
+
+输出
+```text
+foo%2C+bar%3B+baz%3F
+```
diff --git a/docs/source/zh-cn/filters/overview.md b/docs/source/zh-cn/filters/overview.md
index c4bbfc712..58b08b532 100644
--- a/docs/source/zh-cn/filters/overview.md
+++ b/docs/source/zh-cn/filters/overview.md
@@ -11,7 +11,7 @@ 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, normalize_whitespace
-HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
+HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape
 数组 | 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_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
 其他 | default, json, jsonify, inspect, raw, to_integer
diff --git a/docs/source/zh-cn/filters/uri_escape.md b/docs/source/zh-cn/filters/uri_escape.md
new file mode 100644
index 000000000..0caec0c70
--- /dev/null
+++ b/docs/source/zh-cn/filters/uri_escape.md
@@ -0,0 +1,19 @@
+---
+title: uri_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+把 URI 中的特殊字符做百分号编码，空格会变成 `%20`。[保留字][reserved] 不会被转义。
+
+输入
+```liquid
+{{ "http://foo.com/?q=foo, \bar?" | uri_escape }}
+```
+
+输出
+```text
+http://foo.com/?q=foo,%20%5Cbar?
+```
+
+[reserved]: https://en.wikipedia.org/wiki/Percent-encoding#Types_of_URI_characters
diff --git a/docs/source/zh-cn/filters/xml_escape.md b/docs/source/zh-cn/filters/xml_escape.md
new file mode 100644
index 000000000..3fa6d7014
--- /dev/null
+++ b/docs/source/zh-cn/filters/xml_escape.md
@@ -0,0 +1,17 @@
+---
+title: xml_escape
+---
+
+{% since %}v10.13.0{% endsince %}
+
+把文本做 XML 转义。
+
+输入
+```liquid
+{{ "Have you read \'James & the Giant Peach\'?" | xml_escape }}
+```
+
+输出
+```text
+Have you read &#39;James &amp; the Giant Peach&#39;?
+```
diff --git a/src/filters/html.ts b/src/filters/html.ts
index b296dc63d..0dd14d884 100644
--- a/src/filters/html.ts
+++ b/src/filters/html.ts
@@ -19,6 +19,10 @@ export function escape (str: string) {
   return stringify(str).replace(/&|<|>|"|'/g, m => escapeMap[m])
 }
 
+export function xml_escape (str: string) {
+  return escape(str)
+}
+
 function unescape (str: string) {
   return stringify(str).replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
 }
diff --git a/src/filters/url.ts b/src/filters/url.ts
index c5719099e..d2e3b4989 100644
--- a/src/filters/url.ts
+++ b/src/filters/url.ts
@@ -1,4 +1,10 @@
 import { stringify } from '../util/underscore'
 
-export const url_decode = (x: string) => stringify(x).split('+').map(decodeURIComponent).join(' ')
-export const url_encode = (x: string) => stringify(x).split(' ').map(encodeURIComponent).join('+')
+export const url_decode = (x: string) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ')
+export const url_encode = (x: string) => encodeURIComponent(stringify(x)).replace(/%20/g, '+')
+export const cgi_escape = (x: string) => encodeURIComponent(stringify(x))
+  .replace(/%20/g, '+')
+  .replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
+export const uri_escape = (x: string) => encodeURI(stringify(x))
+  .replace(/%5B/g, '[')
+  .replace(/%5D/g, ']')


Test patch:
diff --git a/test/integration/filters/html.spec.ts b/test/integration/filters/html.spec.ts
index 460a7aeb1..fce809d3a 100644
--- a/test/integration/filters/html.spec.ts
+++ b/test/integration/filters/html.spec.ts
@@ -26,6 +26,12 @@ describe('filters/html', function () {
     it('should escape nil value to empty string', () =>
       test('{{ undefinedValue | escape_once }}', ''))
   })
+  describe('xml_escape', function () {
+    it('should xml_escape \' and &', function () {
+      return test('{{ "Have you read \'James & the Giant Peach\'?" | xml_escape }}',
+        'Have you read &#39;James &amp; the Giant Peach&#39;?')
+    })
+  })
   describe('newline_to_br', function () {
     it('should support string_with_newlines', function () {
       const src = '{% capture string_with_newlines %}\n' +
diff --git a/test/integration/filters/url.spec.ts b/test/integration/filters/url.spec.ts
index 6c21d6acc..47b2e5e4d 100644
--- a/test/integration/filters/url.spec.ts
+++ b/test/integration/filters/url.spec.ts
@@ -1,15 +1,45 @@
-import { test } from '../../stub/render'
+import { Liquid } from '../../../src'
 
-describe('filters/url', function () {
-  describe('url_decode', function () {
-    it('should decode %xx and +',
-      () => test('{{ "%27Stop%21%27+said+Fred" | url_decode }}', "'Stop!' said Fred"))
+describe('filters/url', () => {
+  const liquid = new Liquid()
+  describe('url_decode', () => {
+    it('should decode %xx and +', () => {
+      const html = liquid.parseAndRenderSync('{{ "%27Stop%21%27+said+Fred" | url_decode }}')
+      expect(html).toEqual("'Stop!' said Fred")
+    })
   })
 
-  describe('url_encode', function () {
-    it('should encode @',
-      () => test('{{ "john@liquid.com" | url_encode }}', 'john%40liquid.com'))
-    it('should encode <space>',
-      () => test('{{ "Tetsuro Takara" | url_encode }}', 'Tetsuro+Takara'))
+  describe('url_encode', () => {
+    it('should encode @', () => {
+      const html = liquid.parseAndRenderSync('{{ "john@liquid.com" | url_encode }}')
+      expect(html).toEqual('john%40liquid.com')
+    })
+    it('should encode <space>', () => {
+      const html = liquid.parseAndRenderSync('{{ "Tetsuro Takara" | url_encode }}')
+      expect(html).toEqual('Tetsuro+Takara')
+    })
+  })
+
+  describe('cgi_escape', () => {
+    it('should escape CGI chars', () => {
+      const html = liquid.parseAndRenderSync('{{ "!\',()*\\"!" | cgi_escape }}')
+      expect(html).toEqual('%21%27%2C%28%29%2A%22%21')
+    })
+    it('should escape space as +', () => {
+      const html = liquid.parseAndRenderSync('{{ "foo, bar; baz?" | cgi_escape }}')
+      expect(html).toEqual('foo%2C+bar%3B+baz%3F')
+    })
+  })
+
+  describe('uri_escape', () => {
+    it('should escape unsupported chars for uri', () => {
+      const html = liquid.parseAndRenderSync('{{ "http://foo.com/?q=foo, \\\\bar?" | uri_escape }}')
+      expect(html).toEqual('http://foo.com/?q=foo,%20%5Cbar?')
+    })
+    it('should not escape reserved characters', () => {
+      const reserved = "!#$&'()*+,/:;=?@[]"
+      const html = liquid.parseAndRenderSync('{{ reserved | uri_escape }}', { reserved })
+      expect(html).toEqual(reserved)
+    })
   })
 })