From 287ba76c2571b4083834183237a31006c6a26233 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Mon, 13 May 2024 23:30:07 +0800 Subject: [PATCH] feat: slugify filter from Jekyll, #443 --- docs/source/_data/sidebar.yml | 1 + docs/source/filters/overview.md | 2 +- docs/source/filters/slugify.md | 61 +++++++++++++++++++++ docs/source/zh-cn/filters/overview.md | 2 +- docs/source/zh-cn/filters/slugify.md | 59 ++++++++++++++++++++ docs/themes/navy/source/css/_variables.styl | 2 +- src/filters/url.ts | 41 ++++++++++++++ test/integration/filters/url.spec.ts | 49 +++++++++++++++++ 8 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 docs/source/filters/slugify.md create mode 100644 docs/source/zh-cn/filters/slugify.md diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml index 6aa1d0c223..5dfb9a1f91 100644 --- a/docs/source/_data/sidebar.yml +++ b/docs/source/_data/sidebar.yml @@ -82,6 +82,7 @@ filters: shift: shift.html size: size.html slice: slice.html + slugify: slugify.html sort: sort.html sort_natural: sort_natural.html split: split.html diff --git a/docs/source/filters/overview.md b/docs/source/filters/overview.md index 447715e8cb..b32b4c802e 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, number_of_words, array_to_sentence_string -HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape +HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape, slugify 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/slugify.md b/docs/source/filters/slugify.md new file mode 100644 index 0000000000..5172de9a59 --- /dev/null +++ b/docs/source/filters/slugify.md @@ -0,0 +1,61 @@ +--- +title: slugify +--- + +{% since %}v10.13.0{% endsince %} + +Convert a string into a lowercase URL "slug". The slugify filter accepts 2 options, each specifying what to filter. + +1. `mode: string`. The default is `"default"`. They are as follows (with what they filter): + - `"none"`: no characters + - `"raw"`: spaces + - `"default"`: spaces and non-alphanumeric characters + - `"pretty"`: spaces and non-alphanumeric characters except for `._~!$&'()+,;=@` + - `"ascii"`: spaces, non-alphanumeric, and non-ASCII characters + - `"latin"`: like default, except Latin characters are first transliterated (e.g. àèïòü to aeiou). +2. `case: boolean`. The default is `false`. The original case of slug will be retained if set to `true`. + +Input +```liquid +{{ "The _config.yml file" | slugify }} +``` +Output +``` +the-config-yml-file +``` + +Input +```liquid +{{ "The _config.yml file" | slugify: "pretty" }} +``` +Output +``` +the-_config.yml-file +``` + +Input +```liquid +{{ "The _cönfig.yml file" | slugify: "ascii" }} +``` +Output +``` +the-c-nfig-yml-file +``` + +Input +```liquid +{{ "The cönfig.yml file" | slugify: "latin" }} +``` +Output +``` +the-config-yml-file +``` + +Input +```liquid +{{ "The cönfig.yml file" | slugify: "latin", true }} +``` +Output +``` +The-config-yml-file +``` diff --git a/docs/source/zh-cn/filters/overview.md b/docs/source/zh-cn/filters/overview.md index 97d6b21342..3e19c0852d 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, number_of_words, array_to_sentence_string -HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape +HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br, xml_escape, cgi_escape, uri_escape, slugify 数组 | 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/slugify.md b/docs/source/zh-cn/filters/slugify.md new file mode 100644 index 0000000000..dc963b3d46 --- /dev/null +++ b/docs/source/zh-cn/filters/slugify.md @@ -0,0 +1,59 @@ +--- +title: slugify +--- + +将字符串转换为小写的 URL “slug”。`slugify` 过滤器接受两个选项,分别指定要过滤的内容。 + +1. `mode: string`。默认为`"default"`,它可选的值如下: + - `"none"`:没有字符 + - `"raw"`:空格 + - `"default"`:空格和非字母数字字符 + - `"pretty"`:空格和非字母数字字符,但排除 `._~!$&'()+,;=@` + - `"ascii"`:空格、非字母数字和非 ASCII 字符 + - `"latin"`:与默认相同,但拉丁字符首先进行音译(例如,àèïòü 转换为 aeiou)。 +2. `case: boolean`。默认为 `false`。如果为 `true`,则保留 `slug` 原本的大小写。 + +输入 +```liquid +{{ "The _config.yml file" | slugify }} +``` +输出 +``` +the-config-yml-file +``` + +输入 +```liquid +{{ "The _config.yml file" | slugify: "pretty" }} +``` +输出 +``` +the-_config.yml-file +``` + +输入 +```liquid +{{ "The _cönfig.yml file" | slugify: "ascii" }} +``` +输出 +``` +the-c-nfig-yml-file +``` + +输入 +```liquid +{{ "The cönfig.yml file" | slugify: "latin" }} +``` +输出 +``` +the-config-yml-file +``` + +输入 +```liquid +{{ "The cönfig.yml file" | slugify: "latin", true }} +``` +输出 +``` +The-config-yml-file +``` diff --git a/docs/themes/navy/source/css/_variables.styl b/docs/themes/navy/source/css/_variables.styl index 2d6ea544ad..fdf12bdd20 100644 --- a/docs/themes/navy/source/css/_variables.styl +++ b/docs/themes/navy/source/css/_variables.styl @@ -36,7 +36,7 @@ vendor-prefixes = webkit moz ms official font-sans = "Helvetica Neue", Helvetica, Arial, sans-serif font-serif = Garamond, Georgia, "Times New Roman", serif font-mono = "Source Code Pro", Monaco, Menlo, Consolas, monospace -font-size = 15px +font-size = 16px line-height = 1.6em // Layout diff --git a/src/filters/url.ts b/src/filters/url.ts index d2e3b49897..77a9cab1c8 100644 --- a/src/filters/url.ts +++ b/src/filters/url.ts @@ -8,3 +8,44 @@ export const cgi_escape = (x: string) => encodeURIComponent(stringify(x)) export const uri_escape = (x: string) => encodeURI(stringify(x)) .replace(/%5B/g, '[') .replace(/%5D/g, ']') + +const rSlugifyDefault = /[^\p{M}\p{L}\p{Nd}]+/ug +const rSlugifyReplacers = { + 'raw': /\s+/g, + 'default': rSlugifyDefault, + 'pretty': /[^\p{M}\p{L}\p{Nd}._~!$&'()+,;=@]+/ug, + 'ascii': /[^A-Za-z0-9]+/g, + 'latin': rSlugifyDefault, + 'none': null +} + +export function slugify (str: string, mode: keyof typeof rSlugifyReplacers = 'default', cased = false): string { + str = stringify(str) + + const replacer = rSlugifyReplacers[mode] + if (replacer) { + if (mode === 'latin') str = removeAccents(str) + str = str.replace(replacer, '-').replace(/^-|-$/g, '') + } + + return cased ? str : str.toLowerCase() +} + +function removeAccents (str: string): string { + return str.replace(/[àáâãäå]/g, 'a') + .replace(/[æ]/g, 'ae') + .replace(/[ç]/g, 'c') + .replace(/[èéêë]/g, 'e') + .replace(/[ìíîï]/g, 'i') + .replace(/[ð]/g, 'd') + .replace(/[ñ]/g, 'n') + .replace(/[òóôõöø]/g, 'o') + .replace(/[ùúûü]/g, 'u') + .replace(/[ýÿ]/g, 'y') + .replace(/[ß]/g, 'ss') + .replace(/[œ]/g, 'oe') + .replace(/[þ]/g, 'th') + .replace(/[ẞ]/g, 'SS') + .replace(/[Œ]/g, 'OE') + .replace(/[Þ]/g, 'TH') +} diff --git a/test/integration/filters/url.spec.ts b/test/integration/filters/url.spec.ts index 47b2e5e4d4..3dbd67e4f7 100644 --- a/test/integration/filters/url.spec.ts +++ b/test/integration/filters/url.spec.ts @@ -42,4 +42,53 @@ describe('filters/url', () => { expect(html).toEqual(reserved) }) }) + + describe('slugify', () => { + describe('slugify', () => { + it('should slugify with default mode', () => { + const html = liquid.parseAndRenderSync('{{ "The _config.yml file" | slugify }}') + expect(html).toEqual('the-config-yml-file') + }) + + it('should slugify with pretty mode', () => { + const html = liquid.parseAndRenderSync('{{ "The _config.yml file" | slugify: "pretty" }}') + expect(html).toEqual('the-_config.yml-file') + }) + + it('should slugify with ascii mode', () => { + const html = liquid.parseAndRenderSync('{{ "The _cönfig.yml file" | slugify: "ascii" }}') + expect(html).toEqual('the-c-nfig-yml-file') + }) + + it('should slugify with latin mode', () => { + const html = liquid.parseAndRenderSync('{{ "The cönfig.yml file" | slugify: "latin" }}') + expect(html).toEqual('the-config-yml-file') + }) + + it('should slugify with none mode', () => { + const html = liquid.parseAndRenderSync('{{ "The _config.yml file" | slugify: "none" }}') + expect(html).toEqual('the _config.yml file') + }) + + it('should slugify with invalid mode', () => { + const html = liquid.parseAndRenderSync('{{ "The _config.yml file" | slugify: "invalid_mode" }}') + expect(html).toEqual('the _config.yml file') + }) + + it('should slugify with empty string', () => { + const html = liquid.parseAndRenderSync('{{ "" | slugify }}') + expect(html).toEqual('') + }) + + it('should slugify with cased=false', () => { + const html = liquid.parseAndRenderSync('{{ "Test String" | slugify: "pretty", false }}') + expect(html).toEqual('test-string') + }) + + it('should slugify with cased=true', () => { + const html = liquid.parseAndRenderSync('{{ "Test String" | slugify: "pretty", true }}') + expect(html).toEqual('Test-String') + }) + }) + }) })