Skip to content

Commit 9c1d9a0

Browse files
committed
exclude ALMOST ALL symbols and punctuation (more aggressive, using Unicode classes) from filenames
1 parent fc722c3 commit 9c1d9a0

2 files changed

Lines changed: 33 additions & 22 deletions

File tree

src/lib/slugify.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
'use strict';
22

33
// precompile for speed
4-
var HTML_TAGS_REGEX = /<[^>]*>/g; // anything contained in < > tags
5-
var FORBIDDEN_CHARS_REGEX = /[\\/:*?"<>|$%&!@#~.^`'(){}[\],=+;]/g; // Characters in the set: \/:*?"<>|$%&!@#~.^`'(){}[],=+;
6-
var CONTROL_CHARS_REGEX = /\p{Cc}/gu; // Unicode control characters
4+
var HTML_TAGS_REGEX = /<[^>]*>/g; // Anything contained in < > tags
5+
// Forbid ANY character in the Unicode Symbol or Punctuation categories,
6+
// which includes pretty much every common non-word character,
7+
// including all of the following: \/:*?"<>|$%&!@#~.^`'(){}[]-_,=+;
8+
// We actually want to allow hyphens (WORD_SEP_CHAR), but it's not easy
9+
// to exclude them from the regex, so we'll handle them in the match function.
10+
var FORBIDDEN_CHARS_REGEX = /[\p{S}\p{P}]/gu;
11+
// Control chars, format chars (e.g. ZWJ), variation selectors, and the
12+
// combining enclosing keycap. Some of these may be left behind after emoji
13+
// symbols are removed, so we explicitly remove them here.
14+
var INVISIBLE_CHARS_REGEX = /[\p{Cc}\p{Cf}\uFE00-\uFE0F\u20E3]/gu;
715

8-
var UNICODE_REPLACEMENT_CHAR_REGEX = //g; // U+FFFD, the Unicode replacement character
9-
var WHITESPACE_REGEX = /\s+/g;
16+
var UNICODE_REPLACEMENT_CHAR_REGEX = //g; // U+FFFD, the Unicode replacement character
17+
var WHITESPACE_REGEX = /\s+/g; // All whitespace characters
1018

11-
var WORD_SEP_CHAR = '-'; // character used to separate words (replaces whitespace)
19+
var WORD_SEP_CHAR = '-'; // Character used to separate words (replaces whitespace)
1220
var _WORD_SEP_ESCAPED = WORD_SEP_CHAR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13-
var WORD_SEP_CHARS_REGEX = new RegExp(_WORD_SEP_ESCAPED + '{3,}', 'g'); // three or more consecutive word separator chars
14-
var TRAILING_WORD_SEP_CHAR_REGEX = new RegExp(_WORD_SEP_ESCAPED + '$', 'g'); // trailing word separator char
21+
var WORD_SEP_CHARS_REGEX = new RegExp(_WORD_SEP_ESCAPED + '{3,}', 'g'); // Three or more consecutive word separator chars
22+
var TRAILING_WORD_SEP_CHAR_REGEX = new RegExp(_WORD_SEP_ESCAPED + '$', 'g'); // Trailing word separator char
1523

1624
// Safely under the limit for most filesystems
1725
var DEFAULT_MAX_LEN = 60;
@@ -39,15 +47,17 @@ function toWellFormed(str) {
3947
* @return {string}
4048
*/
4149
module.exports = function slugify(str, maxLen = DEFAULT_MAX_LEN) {
42-
var slug = toWellFormed(str ?? '') // Guarantee well-formed Unicode text
43-
.replace(UNICODE_REPLACEMENT_CHAR_REGEX, '') // Drop Unicode replacement chars left by previous step
44-
.replace(HTML_TAGS_REGEX, ' ') // Remove < > tags, such as <br> (replace with space)
45-
.replace(FORBIDDEN_CHARS_REGEX, '') // Remove forbidden filename characters
46-
.toLowerCase() // Lowercase everything
47-
.trim() // Strip leading/trailing whitespace
48-
.replace(WHITESPACE_REGEX, WORD_SEP_CHAR) // Replace any remaining whitespace with the word sep char
49-
.replace(CONTROL_CHARS_REGEX, '') // Remove control characters (after whitespace)
50-
.replace(WORD_SEP_CHARS_REGEX, WORD_SEP_CHAR); // Replace multiple word sep chars with a single one
50+
var slug = toWellFormed(str ?? '') // Guarantee well-formed Unicode text
51+
.replace(UNICODE_REPLACEMENT_CHAR_REGEX, '') // Drop Unicode replacement chars left by previous step
52+
.replace(HTML_TAGS_REGEX, ' ') // Remove < > tags, such as <br> (replace with space)
53+
.replace(FORBIDDEN_CHARS_REGEX, (c) => { // Remove forbidden filename characters (but allow the word sep char)
54+
return c === WORD_SEP_CHAR ? c : '';
55+
})
56+
.toLowerCase() // Lowercase everything
57+
.trim() // Strip leading/trailing whitespace
58+
.replace(WHITESPACE_REGEX, WORD_SEP_CHAR) // Replace any remaining whitespace with the word sep char
59+
.replace(INVISIBLE_CHARS_REGEX, '') // Remove control/format chars and emoji glue (after whitespace)
60+
.replace(WORD_SEP_CHARS_REGEX, WORD_SEP_CHAR); // Replace multiple word sep chars with a single one
5161

5262
if (slug.length <= maxLen) return slug;
5363
// Apply maxLen to the resulting string. Use Array.from().slice() instead of String.prototype.split()

test/jasmine/tests/lib_test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,15 +1956,16 @@ describe('Test lib.js:', function () {
19561956
.toBe('abcdefghijklmnopqrst');
19571957
expect(Lib.slugify('a>b<c)d(e]f[g}h{i')).toBe('abcdefghi');
19581958
expect(Lib.slugify('a=b+c;d')).toBe('abcd');
1959+
expect(Lib.slugify('a_b😀c★d©e')).toBe('abcde');
19591960
});
19601961

1961-
it('removes control characters', function () {
1962+
it('removes control characters and emoji glue', function () {
19621963
expect(Lib.slugify('a\x00b\x07c\x1Fd')).toBe('abcd');
1964+
expect(Lib.slugify('a👨‍👩‍👧b❤️c')).toBe('abc');
19631965
});
19641966

1965-
it('preserves unicode letters (accents, CJK, emoji)', function () {
1967+
it('preserves unicode letters (accents, CJK)', function () {
19661968
expect(Lib.slugify('Café 北京')).toBe('café-北京');
1967-
expect(Lib.slugify('pair😀')).toBe('pair😀');
19681969
});
19691970

19701971
it('drops unpaired surrogates so the result is valid UTF-8', function () {
@@ -1990,8 +1991,8 @@ describe('Test lib.js:', function () {
19901991
});
19911992

19921993
it('never splits a surrogate pair when capping length', function () {
1993-
// each emoji is one code point (a surrogate pair); the cap counts code points
1994-
expect(Lib.slugify('😀😀😀😀😀', 3)).toBe('😀😀😀');
1994+
// each CJK char is one code point (a surrogate pair); the cap counts code points
1995+
expect(Lib.slugify('𠀀𠀀𠀀𠀀𠀀', 3)).toBe('𠀀𠀀𠀀');
19951996
});
19961997
});
19971998

0 commit comments

Comments
 (0)