@mdit-vue/shared 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.5.0](https://github.com/mdit-vue/mdit-vue/compare/v0.4.0...v0.5.0) (2022-07-06)
7
+
8
+ **Note:** Version bump only for package @mdit-vue/shared
9
+
10
+ # [0.4.0](https://github.com/mdit-vue/mdit-vue/compare/v0.3.1...v0.4.0) (2022-07-02)
11
+
12
+ **Note:** Version bump only for package @mdit-vue/shared
13
+
14
+ # [0.3.0](https://github.com/mdit-vue/mdit-vue/compare/v0.2.0...v0.3.0) (2022-06-25)
15
+
16
+ ### Features
17
+
18
+ - **plugin-toc:** add toc plugin ([e64cc2c](https://github.com/mdit-vue/mdit-vue/commit/e64cc2ce3bbc89999d373f8a00fb3a049d6af58f))
19
+ - **shared:** add html escape and unescape utils ([e1103c2](https://github.com/mdit-vue/mdit-vue/commit/e1103c21998aec77c67cdefd3cbb4994c127124c))
20
+
6
21
  # [0.2.0](https://github.com/mdit-vue/mdit-vue/compare/v0.1.1...v0.2.0) (2022-06-23)
7
22
 
8
23
  ### Features
package/README.md CHANGED
@@ -12,6 +12,16 @@ npm i @mdit-vue/shared
12
12
 
13
13
  ## Usage
14
14
 
15
+ Add more properties to `env` via [TypeScript module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation):
16
+
17
+ ```ts
18
+ declare module '@mdit-vue/shared' {
19
+ interface MarkdownItEnv {
20
+ foo?: string[];
21
+ }
22
+ }
23
+ ```
24
+
15
25
  ```ts
16
26
  import MarkdownIt from 'markdown-it';
17
27
  import type { MarkdownItEnv } from '@mdit-vue/shared';
package/dist/index.cjs CHANGED
@@ -1,2 +1,108 @@
1
1
  'use strict';
2
2
 
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ const htmlEscapeMap = {
6
+ "&": "&",
7
+ "<": "&lt;",
8
+ ">": "&gt;",
9
+ "'": "&#39;",
10
+ '"': "&quot;"
11
+ };
12
+ const htmlEscapeRegexp = /[&<>'"]/g;
13
+ const htmlEscape = (str) => str.replace(htmlEscapeRegexp, (char) => htmlEscapeMap[char]);
14
+
15
+ const htmlUnescapeMap = {
16
+ "&amp;": "&",
17
+ "&#38;": "&",
18
+ "&lt;": "<",
19
+ "&#60;": "<",
20
+ "&gt;": ">",
21
+ "&#62;": ">",
22
+ "&apos;": "'",
23
+ "&#39;": "'",
24
+ "&quot;": '"',
25
+ "&#34;": '"'
26
+ };
27
+ const htmlUnescapeRegexp = /&(amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
28
+ const htmlUnescape = (str) => str.replace(htmlUnescapeRegexp, (char) => htmlUnescapeMap[char]);
29
+
30
+ const resolveTitleFromToken = (token, {
31
+ shouldAllowHtml,
32
+ shouldEscapeText
33
+ }) => {
34
+ const children = token.children ?? [];
35
+ const titleTokenTypes = ["text", "emoji", "code_inline"];
36
+ if (shouldAllowHtml) {
37
+ titleTokenTypes.push("html_inline");
38
+ }
39
+ const titleTokens = children.filter((item) => titleTokenTypes.includes(item.type) && !item.meta?.isPermalinkSymbol);
40
+ return titleTokens.reduce((result, item) => {
41
+ if (shouldEscapeText) {
42
+ if (item.type === "code_inline" || item.type === "text") {
43
+ return `${result}${htmlEscape(item.content)}`;
44
+ }
45
+ }
46
+ return `${result}${item.content}`;
47
+ }, "").trim();
48
+ };
49
+
50
+ const resolveHeadersFromTokens = (tokens, {
51
+ level,
52
+ shouldAllowHtml,
53
+ shouldEscapeText,
54
+ slugify,
55
+ format
56
+ }) => {
57
+ const headers = [];
58
+ const stack = [];
59
+ const push = (header) => {
60
+ while (stack.length !== 0 && header.level <= stack[0].level) {
61
+ stack.shift();
62
+ }
63
+ if (stack.length === 0) {
64
+ headers.push(header);
65
+ stack.push(header);
66
+ } else {
67
+ stack[0].children.push(header);
68
+ stack.unshift(header);
69
+ }
70
+ };
71
+ tokens.forEach((_, idx) => {
72
+ const token = tokens[idx];
73
+ if (token?.type !== "heading_open") {
74
+ return;
75
+ }
76
+ const headerLevel = Number.parseInt(token.tag.slice(1), 10);
77
+ if (!level.includes(headerLevel)) {
78
+ return;
79
+ }
80
+ const nextToken = tokens[idx + 1];
81
+ if (!nextToken) {
82
+ return;
83
+ }
84
+ const title = resolveTitleFromToken(nextToken, {
85
+ shouldAllowHtml,
86
+ shouldEscapeText
87
+ });
88
+ const slug = token.attrGet("id") ?? slugify(title);
89
+ push({
90
+ level: headerLevel,
91
+ title: format?.(title) ?? title,
92
+ slug,
93
+ children: []
94
+ });
95
+ });
96
+ return headers;
97
+ };
98
+
99
+ const rControl = /[\u0000-\u001f]/g;
100
+ const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
101
+ const rCombining = /[\u0300-\u036F]/g;
102
+ const slugify = (str) => str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
103
+
104
+ exports.htmlEscape = htmlEscape;
105
+ exports.htmlUnescape = htmlUnescape;
106
+ exports.resolveHeadersFromTokens = resolveHeadersFromTokens;
107
+ exports.resolveTitleFromToken = resolveTitleFromToken;
108
+ exports.slugify = slugify;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,48 @@
1
+ import Token from 'markdown-it/lib/token';
2
+
3
+ /**
4
+ * Escape html chars
5
+ */
6
+ declare const htmlEscape: (str: string) => string;
7
+
8
+ /**
9
+ * Unescape html chars
10
+ */
11
+ declare const htmlUnescape: (str: string) => string;
12
+
1
13
  interface MarkdownItEnv {
2
14
  }
15
+ interface MarkdownItHeader {
16
+ level: number;
17
+ title: string;
18
+ slug: string;
19
+ children: MarkdownItHeader[];
20
+ }
21
+
22
+ /**
23
+ * Resolve headers from markdown-it tokens
24
+ */
25
+ declare const resolveHeadersFromTokens: (tokens: Token[], { level, shouldAllowHtml, shouldEscapeText, slugify, format, }: {
26
+ level: number[];
27
+ shouldAllowHtml: boolean;
28
+ shouldEscapeText: boolean;
29
+ slugify: (str: string) => string;
30
+ format?: ((str: string) => string) | undefined;
31
+ }) => MarkdownItHeader[];
32
+
33
+ /**
34
+ * Resolve header title from markdown-it token
35
+ *
36
+ * Typically using the next token of `heading_open` token
37
+ */
38
+ declare const resolveTitleFromToken: (token: Token, { shouldAllowHtml, shouldEscapeText, }: {
39
+ shouldAllowHtml: boolean;
40
+ shouldEscapeText: boolean;
41
+ }) => string;
42
+
43
+ /**
44
+ * Default slugification function
45
+ */
46
+ declare const slugify: (str: string) => string;
3
47
 
4
- export { MarkdownItEnv };
48
+ export { MarkdownItEnv, MarkdownItHeader, htmlEscape, htmlUnescape, resolveHeadersFromTokens, resolveTitleFromToken, slugify };
package/dist/index.mjs CHANGED
@@ -1 +1,100 @@
1
+ const htmlEscapeMap = {
2
+ "&": "&amp;",
3
+ "<": "&lt;",
4
+ ">": "&gt;",
5
+ "'": "&#39;",
6
+ '"': "&quot;"
7
+ };
8
+ const htmlEscapeRegexp = /[&<>'"]/g;
9
+ const htmlEscape = (str) => str.replace(htmlEscapeRegexp, (char) => htmlEscapeMap[char]);
1
10
 
11
+ const htmlUnescapeMap = {
12
+ "&amp;": "&",
13
+ "&#38;": "&",
14
+ "&lt;": "<",
15
+ "&#60;": "<",
16
+ "&gt;": ">",
17
+ "&#62;": ">",
18
+ "&apos;": "'",
19
+ "&#39;": "'",
20
+ "&quot;": '"',
21
+ "&#34;": '"'
22
+ };
23
+ const htmlUnescapeRegexp = /&(amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
24
+ const htmlUnescape = (str) => str.replace(htmlUnescapeRegexp, (char) => htmlUnescapeMap[char]);
25
+
26
+ const resolveTitleFromToken = (token, {
27
+ shouldAllowHtml,
28
+ shouldEscapeText
29
+ }) => {
30
+ const children = token.children ?? [];
31
+ const titleTokenTypes = ["text", "emoji", "code_inline"];
32
+ if (shouldAllowHtml) {
33
+ titleTokenTypes.push("html_inline");
34
+ }
35
+ const titleTokens = children.filter((item) => titleTokenTypes.includes(item.type) && !item.meta?.isPermalinkSymbol);
36
+ return titleTokens.reduce((result, item) => {
37
+ if (shouldEscapeText) {
38
+ if (item.type === "code_inline" || item.type === "text") {
39
+ return `${result}${htmlEscape(item.content)}`;
40
+ }
41
+ }
42
+ return `${result}${item.content}`;
43
+ }, "").trim();
44
+ };
45
+
46
+ const resolveHeadersFromTokens = (tokens, {
47
+ level,
48
+ shouldAllowHtml,
49
+ shouldEscapeText,
50
+ slugify,
51
+ format
52
+ }) => {
53
+ const headers = [];
54
+ const stack = [];
55
+ const push = (header) => {
56
+ while (stack.length !== 0 && header.level <= stack[0].level) {
57
+ stack.shift();
58
+ }
59
+ if (stack.length === 0) {
60
+ headers.push(header);
61
+ stack.push(header);
62
+ } else {
63
+ stack[0].children.push(header);
64
+ stack.unshift(header);
65
+ }
66
+ };
67
+ tokens.forEach((_, idx) => {
68
+ const token = tokens[idx];
69
+ if (token?.type !== "heading_open") {
70
+ return;
71
+ }
72
+ const headerLevel = Number.parseInt(token.tag.slice(1), 10);
73
+ if (!level.includes(headerLevel)) {
74
+ return;
75
+ }
76
+ const nextToken = tokens[idx + 1];
77
+ if (!nextToken) {
78
+ return;
79
+ }
80
+ const title = resolveTitleFromToken(nextToken, {
81
+ shouldAllowHtml,
82
+ shouldEscapeText
83
+ });
84
+ const slug = token.attrGet("id") ?? slugify(title);
85
+ push({
86
+ level: headerLevel,
87
+ title: format?.(title) ?? title,
88
+ slug,
89
+ children: []
90
+ });
91
+ });
92
+ return headers;
93
+ };
94
+
95
+ const rControl = /[\u0000-\u001f]/g;
96
+ const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
97
+ const rCombining = /[\u0300-\u036F]/g;
98
+ const slugify = (str) => str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
99
+
100
+ export { htmlEscape, htmlUnescape, resolveHeadersFromTokens, resolveTitleFromToken, slugify };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdit-vue/shared",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared utils and types of mdit-vue",
5
5
  "keywords": [
6
6
  "markdown-it",
@@ -30,11 +30,17 @@
30
30
  "build": "unbuild",
31
31
  "test": "vitest"
32
32
  },
33
+ "dependencies": {
34
+ "@types/markdown-it": "^12.2.3",
35
+ "markdown-it": "^13.0.1"
36
+ },
33
37
  "devDependencies": {
34
- "unbuild": "^0.7.4"
38
+ "c8": "^7.11.3",
39
+ "unbuild": "^0.7.4",
40
+ "vitest": "^0.17.0"
35
41
  },
36
42
  "publishConfig": {
37
43
  "access": "public"
38
44
  },
39
- "gitHead": "4b796710a93d6126e0c59e9ccc8cd742d5f121ea"
45
+ "gitHead": "33671b4a15611ea35b065269526449129eef99d6"
40
46
  }
@@ -0,0 +1,18 @@
1
+ const htmlEscapeMap = {
2
+ '&': '&amp;',
3
+ '<': '&lt;',
4
+ '>': '&gt;',
5
+ "'": '&#39;',
6
+ '"': '&quot;',
7
+ };
8
+
9
+ const htmlEscapeRegexp = /[&<>'"]/g;
10
+
11
+ /**
12
+ * Escape html chars
13
+ */
14
+ export const htmlEscape = (str: string): string =>
15
+ str.replace(
16
+ htmlEscapeRegexp,
17
+ (char) => htmlEscapeMap[char as keyof typeof htmlEscapeMap],
18
+ );
@@ -0,0 +1,23 @@
1
+ const htmlUnescapeMap = {
2
+ '&amp;': '&',
3
+ '&#38;': '&',
4
+ '&lt;': '<',
5
+ '&#60;': '<',
6
+ '&gt;': '>',
7
+ '&#62;': '>',
8
+ '&apos;': "'",
9
+ '&#39;': "'",
10
+ '&quot;': '"',
11
+ '&#34;': '"',
12
+ };
13
+
14
+ const htmlUnescapeRegexp = /&(amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
15
+
16
+ /**
17
+ * Unescape html chars
18
+ */
19
+ export const htmlUnescape = (str: string): string =>
20
+ str.replace(
21
+ htmlUnescapeRegexp,
22
+ (char) => htmlUnescapeMap[char as keyof typeof htmlUnescapeMap],
23
+ );
package/src/index.ts CHANGED
@@ -1 +1,6 @@
1
+ export * from './html-escape';
2
+ export * from './html-unescape';
3
+ export * from './resolve-headers-from-tokens';
4
+ export * from './resolve-title-from-token';
5
+ export * from './slugify';
1
6
  export * from './types';
@@ -0,0 +1,88 @@
1
+ import type Token from 'markdown-it/lib/token';
2
+ import { resolveTitleFromToken } from './resolve-title-from-token';
3
+ import type { MarkdownItHeader } from './types';
4
+
5
+ /**
6
+ * Resolve headers from markdown-it tokens
7
+ */
8
+ export const resolveHeadersFromTokens = (
9
+ tokens: Token[],
10
+ {
11
+ level,
12
+ shouldAllowHtml,
13
+ shouldEscapeText,
14
+ slugify,
15
+ format,
16
+ }: {
17
+ level: number[];
18
+ shouldAllowHtml: boolean;
19
+ shouldEscapeText: boolean;
20
+ slugify: (str: string) => string;
21
+ format?: (str: string) => string;
22
+ },
23
+ ): MarkdownItHeader[] => {
24
+ // store the result of headers
25
+ const headers: MarkdownItHeader[] = [];
26
+
27
+ // a temp headers stack for generating the headers tree
28
+ const stack: MarkdownItHeader[] = [];
29
+
30
+ // push a header to the headers tree
31
+ const push = (header: MarkdownItHeader): void => {
32
+ while (stack.length !== 0 && header.level <= stack[0].level) {
33
+ stack.shift();
34
+ }
35
+
36
+ if (stack.length === 0) {
37
+ headers.push(header);
38
+ stack.push(header);
39
+ } else {
40
+ stack[0].children.push(header);
41
+ stack.unshift(header);
42
+ }
43
+ };
44
+
45
+ tokens.forEach((_, idx) => {
46
+ const token = tokens[idx];
47
+
48
+ // if the token type does not match, skip
49
+ if (token?.type !== 'heading_open') {
50
+ return;
51
+ }
52
+
53
+ // get the level from the tag, h1 -> 1
54
+ const headerLevel = Number.parseInt(token.tag.slice(1), 10);
55
+
56
+ // if the level should not be extracted, skip
57
+ if (!level.includes(headerLevel)) {
58
+ return;
59
+ }
60
+
61
+ // the next token of 'heading_open' contains the heading content
62
+ const nextToken = tokens[idx + 1];
63
+
64
+ // if the next token does not exist, skip
65
+ if (!nextToken) {
66
+ return;
67
+ }
68
+
69
+ const title = resolveTitleFromToken(nextToken, {
70
+ shouldAllowHtml,
71
+ shouldEscapeText,
72
+ });
73
+
74
+ // the id of the heading anchor is the slugify result of markdown-it-anchor
75
+ // if the id does not exist, slugify the title ourselves
76
+ const slug = token.attrGet('id') ?? slugify(title);
77
+
78
+ // push the header to tree
79
+ push({
80
+ level: headerLevel,
81
+ title: format?.(title) ?? title,
82
+ slug,
83
+ children: [],
84
+ });
85
+ });
86
+
87
+ return headers;
88
+ };
@@ -0,0 +1,52 @@
1
+ import type Token from 'markdown-it/lib/token';
2
+ import { htmlEscape } from './html-escape';
3
+
4
+ /**
5
+ * Resolve header title from markdown-it token
6
+ *
7
+ * Typically using the next token of `heading_open` token
8
+ */
9
+ export const resolveTitleFromToken = (
10
+ token: Token,
11
+ {
12
+ shouldAllowHtml,
13
+ shouldEscapeText,
14
+ }: {
15
+ shouldAllowHtml: boolean;
16
+ shouldEscapeText: boolean;
17
+ },
18
+ ): string => {
19
+ // children of the token contains the parsed result of the heading title
20
+ const children = token.children ?? [];
21
+
22
+ // type of tokens to be included in the heading title
23
+ const titleTokenTypes = ['text', 'emoji', 'code_inline'];
24
+
25
+ // include 'html_inline' or not
26
+ if (shouldAllowHtml) {
27
+ titleTokenTypes.push('html_inline');
28
+ }
29
+
30
+ // filter the token type to be included in the title
31
+ const titleTokens = children.filter(
32
+ (item) =>
33
+ titleTokenTypes.includes(item.type) &&
34
+ // filter permalink symbol that generated by markdown-it-anchor
35
+ !item.meta?.isPermalinkSymbol,
36
+ );
37
+
38
+ // get title from tokens
39
+ return titleTokens
40
+ .reduce((result, item) => {
41
+ if (shouldEscapeText) {
42
+ // escape the content of 'code_inline' and 'text'
43
+ if (item.type === 'code_inline' || item.type === 'text') {
44
+ return `${result}${htmlEscape(item.content)}`;
45
+ }
46
+ }
47
+
48
+ // keep the content of 'emoji' and 'html_inline'
49
+ return `${result}${item.content}`;
50
+ }, '')
51
+ .trim();
52
+ };
package/src/slugify.ts ADDED
@@ -0,0 +1,25 @@
1
+ // eslint-disable-next-line no-control-regex
2
+ const rControl = /[\u0000-\u001f]/g;
3
+ const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
4
+ const rCombining = /[\u0300-\u036F]/g;
5
+
6
+ /**
7
+ * Default slugification function
8
+ */
9
+ export const slugify = (str: string): string =>
10
+ str
11
+ .normalize('NFKD')
12
+ // Remove accents
13
+ .replace(rCombining, '')
14
+ // Remove control characters
15
+ .replace(rControl, '')
16
+ // Replace special characters
17
+ .replace(rSpecial, '-')
18
+ // Remove continuos separators
19
+ .replace(/-{2,}/g, '-')
20
+ // Remove prefixing and trailing separators
21
+ .replace(/^-+|-+$/g, '')
22
+ // ensure it doesn't start with a number (#121)
23
+ .replace(/^(\d)/, '_$1')
24
+ // lowercase
25
+ .toLowerCase();
package/src/types.ts CHANGED
@@ -2,3 +2,10 @@
2
2
  export interface MarkdownItEnv {
3
3
  // to be extended with module augmentation
4
4
  }
5
+
6
+ export interface MarkdownItHeader {
7
+ level: number;
8
+ title: string;
9
+ slug: string;
10
+ children: MarkdownItHeader[];
11
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { htmlEscape } from '../src';
3
+
4
+ const testCases = [['&<>\'"', '&amp;&lt;&gt;&#39;&quot;']];
5
+
6
+ describe('shared > html-escape', () => {
7
+ describe('should escape special chars', () => {
8
+ testCases.forEach(([source, expected]) => {
9
+ it(source, () => {
10
+ expect(htmlEscape(source)).toBe(expected);
11
+ });
12
+ });
13
+ });
14
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { htmlUnescape } from '../src';
3
+
4
+ const testCases = [
5
+ ['&amp;&#38;&lt;&#60;&gt;&#62;&apos;&#39;&quot;&#34;', '&&<<>>\'\'""'],
6
+ ['&amp;lt;', '&lt;'],
7
+ [
8
+ '&amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;',
9
+ '&lt;script&gt;&lt;/script&gt;',
10
+ ],
11
+ ];
12
+
13
+ describe('shared > html-unescape', () => {
14
+ describe('should unescape special chars', () => {
15
+ testCases.forEach(([source, expected]) => {
16
+ it(source, () => {
17
+ expect(htmlUnescape(source)).toBe(expected);
18
+ });
19
+ });
20
+ });
21
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { slugify } from '../src';
3
+
4
+ const testCases: [string, string][] = [
5
+ ['Привет', 'привет'],
6
+ ['Лед üäöß', 'лед-uaoß'],
7
+ ['hangul 가', 'hangul-가'],
8
+ ['ع', 'ع'],
9
+ ['džℍΩ', 'dzhω'],
10
+ ['カi⁹', 'カi9'],
11
+ // ㌀ -> アパート'
12
+ ['㌀', decodeURIComponent('%E3%82%A2%E3%83%8F%E3%82%9A%E3%83%BC%E3%83%88')],
13
+ ['¼', '_1⁄4'],
14
+ ['džℍΩカi⁹¼', 'dzhωカi91⁄4'],
15
+ ['Iлtèrnåtïonɑlíƶatï߀ԉ', 'iлternationɑliƶati߀ԉ'],
16
+ [
17
+ 'Båcòл ípѕùm ðoɭ߀r ѕït aϻèt âùþê aԉᏧ߀üïlɭê ƃëéf culρá fïlèt ϻiǥnòn cuρiᏧatat ut êлim tòлɢùê.',
18
+ 'bacoл-ipѕum-ðoɭ߀r-ѕit-aϻet-auþe-aԉꮷ߀uilɭe-ƃeef-culρa-filet-ϻiǥnon-cuρiꮷatat-ut-eлim-toлɢue',
19
+ ],
20
+ ['ᴎᴑᴅᴇȷʂ', 'ᴎᴑᴅᴇȷʂ'],
21
+ ['hambúrguer', 'hamburguer'],
22
+ ['hŒllœ', 'hœllœ'],
23
+ ['Fußball', 'fußball'],
24
+ ['ABCDEFGHIJKLMNOPQRSTUVWXYZé', 'abcdefghijklmnopqrstuvwxyze'],
25
+ ];
26
+
27
+ describe('shared > slugify', () => {
28
+ describe('should slugify string correctly', () => {
29
+ testCases.forEach(([source, expected]) => {
30
+ it(`${source} => ${expected}`, () => {
31
+ expect(slugify(source)).toBe(expected);
32
+ });
33
+ });
34
+ });
35
+ });