@cjser/filenamify 7.0.1-cjser.2
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/dist-cjser/browser.cjs +113 -0
- package/dist-cjser/index.cjs +127 -0
- package/filenamify-path.d.ts +16 -0
- package/filenamify-path.js +7 -0
- package/filenamify.d.ts +39 -0
- package/filenamify.js +128 -0
- package/index.d.ts +3 -0
- package/index.js +2 -0
- package/license +9 -0
- package/package.json +100 -0
- package/readme.md +92 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// packages/@cjser/filenamify.tmp-26-1778150437771/filenamify.js
|
|
30
|
+
var filenamify_exports = {};
|
|
31
|
+
__export(filenamify_exports, {
|
|
32
|
+
default: () => filenamify
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(filenamify_exports);
|
|
35
|
+
var import_filename_reserved_regex = __toESM(require("@cjser/filename-reserved-regex"), 1);
|
|
36
|
+
var MAX_FILENAME_LENGTH = 100;
|
|
37
|
+
var reRelativePath = /^\.+(\\|\/)|^\.+$/;
|
|
38
|
+
var reTrailingDotsAndSpaces = /[. ]+$/;
|
|
39
|
+
var reControlChars = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/gu;
|
|
40
|
+
var reControlCharsTest = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/u;
|
|
41
|
+
var isZeroWidthJoiner = (char) => char === "\u200D";
|
|
42
|
+
var reRepeatedReservedCharacters = /([<>:"/\\|?*\u0000-\u001F]){2,}/g;
|
|
43
|
+
var reReplacementReservedCharacters = /[<>:"/\\|?*\u0000-\u001F]/;
|
|
44
|
+
var reUnicodeWhitespace = /[\t\n\r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
|
|
45
|
+
var segmenter;
|
|
46
|
+
function getSegmenter() {
|
|
47
|
+
segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
48
|
+
return segmenter;
|
|
49
|
+
}
|
|
50
|
+
function truncateFilename(filename, maxLength) {
|
|
51
|
+
if (filename.length <= maxLength) {
|
|
52
|
+
return filename;
|
|
53
|
+
}
|
|
54
|
+
const extensionIndex = filename.lastIndexOf(".");
|
|
55
|
+
if (extensionIndex === -1) {
|
|
56
|
+
return truncateByGraphemeBudget(filename, maxLength);
|
|
57
|
+
}
|
|
58
|
+
const base = filename.slice(0, extensionIndex);
|
|
59
|
+
const extension = filename.slice(extensionIndex);
|
|
60
|
+
const baseBudget = Math.max(0, maxLength - extension.length);
|
|
61
|
+
const truncatedBase = truncateByGraphemeBudget(base, baseBudget);
|
|
62
|
+
return truncatedBase.replace(/ +$/, "") + extension;
|
|
63
|
+
}
|
|
64
|
+
function filenamify(string, options = {}) {
|
|
65
|
+
if (typeof string !== "string") {
|
|
66
|
+
throw new TypeError("Expected a string");
|
|
67
|
+
}
|
|
68
|
+
const replacement = options.replacement ?? "!";
|
|
69
|
+
const hasReservedChars = reReplacementReservedCharacters.test(replacement);
|
|
70
|
+
const hasControlChars = [...replacement].some((char) => reControlCharsTest.test(char) && !isZeroWidthJoiner(char));
|
|
71
|
+
if (hasReservedChars || hasControlChars) {
|
|
72
|
+
throw new Error("Replacement string cannot contain reserved filename characters");
|
|
73
|
+
}
|
|
74
|
+
string = string.normalize("NFC");
|
|
75
|
+
string = string.replaceAll(reUnicodeWhitespace, " ");
|
|
76
|
+
if (replacement.length > 0) {
|
|
77
|
+
string = string.replaceAll(reRepeatedReservedCharacters, "$1");
|
|
78
|
+
}
|
|
79
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
80
|
+
string = string.replace(reRelativePath, replacement);
|
|
81
|
+
string = string.replace((0, import_filename_reserved_regex.default)(), replacement);
|
|
82
|
+
string = string.replaceAll(reControlChars, (char) => isZeroWidthJoiner(char) ? char : replacement);
|
|
83
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
84
|
+
if (string.length === 0) {
|
|
85
|
+
string = replacement.replace(reTrailingDotsAndSpaces, "");
|
|
86
|
+
if (string.length === 0 && replacement.length > 0) {
|
|
87
|
+
string = "!";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const allowedLength = typeof options.maxLength === "number" ? options.maxLength : MAX_FILENAME_LENGTH;
|
|
91
|
+
string = truncateFilename(string, allowedLength);
|
|
92
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
93
|
+
if ((0, import_filename_reserved_regex.windowsReservedNameRegex)().test(string)) {
|
|
94
|
+
string += replacement;
|
|
95
|
+
}
|
|
96
|
+
return string;
|
|
97
|
+
}
|
|
98
|
+
function truncateByGraphemeBudget(input, budget) {
|
|
99
|
+
if (input.length <= budget) {
|
|
100
|
+
return input;
|
|
101
|
+
}
|
|
102
|
+
let count = 0;
|
|
103
|
+
let output = "";
|
|
104
|
+
for (const { segment } of getSegmenter().segment(input)) {
|
|
105
|
+
const next = count + segment.length;
|
|
106
|
+
if (next > budget) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
output += segment;
|
|
110
|
+
count = next;
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// packages/@cjser/filenamify.tmp-26-1778150437771/index.js
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
default: () => filenamify,
|
|
33
|
+
filenamifyPath: () => filenamifyPath
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// packages/@cjser/filenamify.tmp-26-1778150437771/filenamify.js
|
|
38
|
+
var import_filename_reserved_regex = __toESM(require("@cjser/filename-reserved-regex"), 1);
|
|
39
|
+
var MAX_FILENAME_LENGTH = 100;
|
|
40
|
+
var reRelativePath = /^\.+(\\|\/)|^\.+$/;
|
|
41
|
+
var reTrailingDotsAndSpaces = /[. ]+$/;
|
|
42
|
+
var reControlChars = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/gu;
|
|
43
|
+
var reControlCharsTest = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/u;
|
|
44
|
+
var isZeroWidthJoiner = (char) => char === "\u200D";
|
|
45
|
+
var reRepeatedReservedCharacters = /([<>:"/\\|?*\u0000-\u001F]){2,}/g;
|
|
46
|
+
var reReplacementReservedCharacters = /[<>:"/\\|?*\u0000-\u001F]/;
|
|
47
|
+
var reUnicodeWhitespace = /[\t\n\r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
|
|
48
|
+
var segmenter;
|
|
49
|
+
function getSegmenter() {
|
|
50
|
+
segmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
51
|
+
return segmenter;
|
|
52
|
+
}
|
|
53
|
+
function truncateFilename(filename, maxLength) {
|
|
54
|
+
if (filename.length <= maxLength) {
|
|
55
|
+
return filename;
|
|
56
|
+
}
|
|
57
|
+
const extensionIndex = filename.lastIndexOf(".");
|
|
58
|
+
if (extensionIndex === -1) {
|
|
59
|
+
return truncateByGraphemeBudget(filename, maxLength);
|
|
60
|
+
}
|
|
61
|
+
const base = filename.slice(0, extensionIndex);
|
|
62
|
+
const extension = filename.slice(extensionIndex);
|
|
63
|
+
const baseBudget = Math.max(0, maxLength - extension.length);
|
|
64
|
+
const truncatedBase = truncateByGraphemeBudget(base, baseBudget);
|
|
65
|
+
return truncatedBase.replace(/ +$/, "") + extension;
|
|
66
|
+
}
|
|
67
|
+
function filenamify(string, options = {}) {
|
|
68
|
+
if (typeof string !== "string") {
|
|
69
|
+
throw new TypeError("Expected a string");
|
|
70
|
+
}
|
|
71
|
+
const replacement = options.replacement ?? "!";
|
|
72
|
+
const hasReservedChars = reReplacementReservedCharacters.test(replacement);
|
|
73
|
+
const hasControlChars = [...replacement].some((char) => reControlCharsTest.test(char) && !isZeroWidthJoiner(char));
|
|
74
|
+
if (hasReservedChars || hasControlChars) {
|
|
75
|
+
throw new Error("Replacement string cannot contain reserved filename characters");
|
|
76
|
+
}
|
|
77
|
+
string = string.normalize("NFC");
|
|
78
|
+
string = string.replaceAll(reUnicodeWhitespace, " ");
|
|
79
|
+
if (replacement.length > 0) {
|
|
80
|
+
string = string.replaceAll(reRepeatedReservedCharacters, "$1");
|
|
81
|
+
}
|
|
82
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
83
|
+
string = string.replace(reRelativePath, replacement);
|
|
84
|
+
string = string.replace((0, import_filename_reserved_regex.default)(), replacement);
|
|
85
|
+
string = string.replaceAll(reControlChars, (char) => isZeroWidthJoiner(char) ? char : replacement);
|
|
86
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
87
|
+
if (string.length === 0) {
|
|
88
|
+
string = replacement.replace(reTrailingDotsAndSpaces, "");
|
|
89
|
+
if (string.length === 0 && replacement.length > 0) {
|
|
90
|
+
string = "!";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const allowedLength = typeof options.maxLength === "number" ? options.maxLength : MAX_FILENAME_LENGTH;
|
|
94
|
+
string = truncateFilename(string, allowedLength);
|
|
95
|
+
string = string.replace(reTrailingDotsAndSpaces, "");
|
|
96
|
+
if ((0, import_filename_reserved_regex.windowsReservedNameRegex)().test(string)) {
|
|
97
|
+
string += replacement;
|
|
98
|
+
}
|
|
99
|
+
return string;
|
|
100
|
+
}
|
|
101
|
+
function truncateByGraphemeBudget(input, budget) {
|
|
102
|
+
if (input.length <= budget) {
|
|
103
|
+
return input;
|
|
104
|
+
}
|
|
105
|
+
let count = 0;
|
|
106
|
+
let output = "";
|
|
107
|
+
for (const { segment } of getSegmenter().segment(input)) {
|
|
108
|
+
const next = count + segment.length;
|
|
109
|
+
if (next > budget) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
output += segment;
|
|
113
|
+
count = next;
|
|
114
|
+
}
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// packages/@cjser/filenamify.tmp-26-1778150437771/filenamify-path.js
|
|
119
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
120
|
+
function filenamifyPath(filePath, options) {
|
|
121
|
+
filePath = import_node_path.default.resolve(filePath);
|
|
122
|
+
return import_node_path.default.join(import_node_path.default.dirname(filePath), filenamify(import_node_path.default.basename(filePath), options));
|
|
123
|
+
}
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
filenamifyPath
|
|
127
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {type Options} from './filenamify.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Convert the filename in a path to a valid filename and return the augmented path.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
```
|
|
8
|
+
import {filenamifyPath} from '@cjser/filenamify';
|
|
9
|
+
|
|
10
|
+
filenamifyPath('foo:bar');
|
|
11
|
+
//=> 'foo!bar'
|
|
12
|
+
```
|
|
13
|
+
*/
|
|
14
|
+
export default function filenamifyPath(path: string, options?: Options): string;
|
|
15
|
+
|
|
16
|
+
export type {Options} from './filenamify.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import filenamify from './filenamify.js';
|
|
3
|
+
|
|
4
|
+
export default function filenamifyPath(filePath, options) {
|
|
5
|
+
filePath = path.resolve(filePath);
|
|
6
|
+
return path.join(path.dirname(filePath), filenamify(path.basename(filePath), options));
|
|
7
|
+
}
|
package/filenamify.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type Options = {
|
|
2
|
+
/**
|
|
3
|
+
String to use as replacement for reserved filename characters.
|
|
4
|
+
|
|
5
|
+
Cannot contain: `<` `>` `:` `"` `/` `\` `|` `?` `*` or control characters.
|
|
6
|
+
|
|
7
|
+
@default '!'
|
|
8
|
+
*/
|
|
9
|
+
readonly replacement?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Truncate the filename to the given length.
|
|
13
|
+
|
|
14
|
+
Only the base of the filename is truncated, preserving the extension. If the extension itself is longer than `maxLength`, you will get a string that is longer than `maxLength`, so you need to check for that if you allow arbitrary extensions.
|
|
15
|
+
|
|
16
|
+
Truncation is grapheme-aware and will not split Unicode characters (surrogate pairs or extended grapheme clusters). If the remaining budget (after accounting for the extension) is smaller than a whole grapheme, the base filename may be truncated to an empty string to avoid splitting.
|
|
17
|
+
|
|
18
|
+
Systems generally allow up to 255 characters, but we default to 100 for usability reasons.
|
|
19
|
+
|
|
20
|
+
@default 100
|
|
21
|
+
*/
|
|
22
|
+
readonly maxLength?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
Convert a string to a valid filename.
|
|
27
|
+
|
|
28
|
+
@example
|
|
29
|
+
```
|
|
30
|
+
import filenamify from '@cjser/filenamify';
|
|
31
|
+
|
|
32
|
+
filenamify('<foo/bar>');
|
|
33
|
+
//=> '!foo!bar!'
|
|
34
|
+
|
|
35
|
+
filenamify('foo:"bar"', {replacement: '🐴'});
|
|
36
|
+
//=> 'foo🐴bar🐴'
|
|
37
|
+
```
|
|
38
|
+
*/
|
|
39
|
+
export default function filenamify(string: string, options?: Options): string;
|
package/filenamify.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import filenameReservedRegex, {windowsReservedNameRegex} from '@cjser/filename-reserved-regex';
|
|
2
|
+
|
|
3
|
+
// Doesn't make sense to have longer filenames
|
|
4
|
+
const MAX_FILENAME_LENGTH = 100;
|
|
5
|
+
|
|
6
|
+
const reRelativePath = /^\.+(\\|\/)|^\.+$/;
|
|
7
|
+
const reTrailingDotsAndSpaces = /[. ]+$/;
|
|
8
|
+
|
|
9
|
+
// Remove all problematic characters except zero-width joiner (\u200D) needed for emoji
|
|
10
|
+
const reControlChars = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/gu;
|
|
11
|
+
const reControlCharsTest = /[\p{Control}\p{Format}\p{Zl}\p{Zp}\uFFF0-\uFFFF]/u;
|
|
12
|
+
const isZeroWidthJoiner = char => char === '\u200D';
|
|
13
|
+
const reRepeatedReservedCharacters = /([<>:"/\\|?*\u0000-\u001F]){2,}/g; // eslint-disable-line no-control-regex
|
|
14
|
+
|
|
15
|
+
// For validating replacement string - only truly reserved characters, not trailing spaces/periods
|
|
16
|
+
const reReplacementReservedCharacters = /[<>:"/\\|?*\u0000-\u001F]/; // eslint-disable-line no-control-regex
|
|
17
|
+
|
|
18
|
+
// Normalize various Unicode whitespace characters to regular space
|
|
19
|
+
// Using specific characters instead of \s to avoid matching regular spaces
|
|
20
|
+
const reUnicodeWhitespace = /[\t\n\r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
|
|
21
|
+
|
|
22
|
+
let segmenter;
|
|
23
|
+
function getSegmenter() {
|
|
24
|
+
segmenter ??= new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
|
25
|
+
return segmenter;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncateFilename(filename, maxLength) {
|
|
29
|
+
if (filename.length <= maxLength) {
|
|
30
|
+
return filename;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const extensionIndex = filename.lastIndexOf('.');
|
|
34
|
+
|
|
35
|
+
// No extension - simple truncation
|
|
36
|
+
if (extensionIndex === -1) {
|
|
37
|
+
return truncateByGraphemeBudget(filename, maxLength);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Has extension - preserve it and truncate base
|
|
41
|
+
const base = filename.slice(0, extensionIndex);
|
|
42
|
+
const extension = filename.slice(extensionIndex);
|
|
43
|
+
const baseBudget = Math.max(0, maxLength - extension.length);
|
|
44
|
+
const truncatedBase = truncateByGraphemeBudget(base, baseBudget);
|
|
45
|
+
|
|
46
|
+
// Strip trailing spaces from base (not periods - they're not trailing in final filename)
|
|
47
|
+
return truncatedBase.replace(/ +$/, '') + extension;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function filenamify(string, options = {}) {
|
|
51
|
+
if (typeof string !== 'string') {
|
|
52
|
+
throw new TypeError('Expected a string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const replacement = options.replacement ?? '!';
|
|
56
|
+
|
|
57
|
+
const hasReservedChars = reReplacementReservedCharacters.test(replacement);
|
|
58
|
+
const hasControlChars = [...replacement].some(char => reControlCharsTest.test(char) && !isZeroWidthJoiner(char));
|
|
59
|
+
|
|
60
|
+
if (hasReservedChars || hasControlChars) {
|
|
61
|
+
throw new Error('Replacement string cannot contain reserved filename characters');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Normalize to NFC first to stabilize byte representation and length calculations across platforms.
|
|
65
|
+
string = string.normalize('NFC');
|
|
66
|
+
|
|
67
|
+
// Normalize Unicode whitespace to single spaces
|
|
68
|
+
string = string.replaceAll(reUnicodeWhitespace, ' ');
|
|
69
|
+
|
|
70
|
+
if (replacement.length > 0) {
|
|
71
|
+
string = string.replaceAll(reRepeatedReservedCharacters, '$1');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Trim trailing spaces and periods (Windows rule) - do this BEFORE replacements
|
|
75
|
+
// so they get stripped rather than replaced
|
|
76
|
+
string = string.replace(reTrailingDotsAndSpaces, '');
|
|
77
|
+
|
|
78
|
+
string = string.replace(reRelativePath, replacement);
|
|
79
|
+
string = string.replace(filenameReservedRegex(), replacement);
|
|
80
|
+
string = string.replaceAll(reControlChars, char => isZeroWidthJoiner(char) ? char : replacement);
|
|
81
|
+
|
|
82
|
+
// Trim trailing spaces and periods again (in case replacement created new ones)
|
|
83
|
+
string = string.replace(reTrailingDotsAndSpaces, '');
|
|
84
|
+
|
|
85
|
+
// If the string is now empty, use replacement with trailing spaces/periods stripped
|
|
86
|
+
if (string.length === 0) {
|
|
87
|
+
string = replacement.replace(reTrailingDotsAndSpaces, '');
|
|
88
|
+
// If still empty and replacement wasn't explicitly empty, use '!' as fallback
|
|
89
|
+
if (string.length === 0 && replacement.length > 0) {
|
|
90
|
+
string = '!';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Truncate before Windows reserved name check (truncation can create reserved names)
|
|
95
|
+
const allowedLength = typeof options.maxLength === 'number' ? options.maxLength : MAX_FILENAME_LENGTH;
|
|
96
|
+
string = truncateFilename(string, allowedLength);
|
|
97
|
+
|
|
98
|
+
// Strip trailing spaces/periods after truncation (truncation can create them)
|
|
99
|
+
string = string.replace(reTrailingDotsAndSpaces, '');
|
|
100
|
+
|
|
101
|
+
// Check for Windows reserved names after truncation and stripping
|
|
102
|
+
// Windows compatibility takes precedence over maxLength, so we add suffix even if it exceeds limit
|
|
103
|
+
if (windowsReservedNameRegex().test(string)) {
|
|
104
|
+
string += replacement;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function truncateByGraphemeBudget(input, budget) {
|
|
111
|
+
if (input.length <= budget) {
|
|
112
|
+
return input;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let count = 0;
|
|
116
|
+
let output = '';
|
|
117
|
+
for (const {segment} of getSegmenter().segment(input)) {
|
|
118
|
+
const next = count + segment.length;
|
|
119
|
+
if (next > budget) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
output += segment;
|
|
124
|
+
count = next;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return output;
|
|
128
|
+
}
|
package/index.d.ts
ADDED
package/index.js
ADDED
package/license
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cjser/filenamify",
|
|
3
|
+
"version": "7.0.1-cjser.2",
|
|
4
|
+
"description": "Convert a string to a valid safe filename",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://code.moenext.com/3rdeye/cjser.git"
|
|
9
|
+
},
|
|
10
|
+
"funding": "https://github.com/sponsors/sindresorhus",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Sindre Sorhus",
|
|
13
|
+
"email": "sindresorhus@gmail.com",
|
|
14
|
+
"url": "https://sindresorhus.com"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./index.d.ts",
|
|
20
|
+
"require": "./dist-cjser/index.cjs",
|
|
21
|
+
"default": "./index.js"
|
|
22
|
+
},
|
|
23
|
+
"./browser": {
|
|
24
|
+
"types": "./filenamify.d.ts",
|
|
25
|
+
"require": "./dist-cjser/browser.cjs",
|
|
26
|
+
"default": "./filenamify.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "xo && ava"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"filenamify-path.d.ts",
|
|
38
|
+
"filenamify-path.js",
|
|
39
|
+
"filenamify.d.ts",
|
|
40
|
+
"filenamify.js",
|
|
41
|
+
"index.d.ts",
|
|
42
|
+
"index.js",
|
|
43
|
+
"dist-cjser"
|
|
44
|
+
],
|
|
45
|
+
"keywords": [
|
|
46
|
+
"filename",
|
|
47
|
+
"safe",
|
|
48
|
+
"sanitize",
|
|
49
|
+
"file",
|
|
50
|
+
"name",
|
|
51
|
+
"string",
|
|
52
|
+
"path",
|
|
53
|
+
"filepath",
|
|
54
|
+
"convert",
|
|
55
|
+
"valid",
|
|
56
|
+
"dirname"
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@cjser/filename-reserved-regex": "4.0.0-cjser.2"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"ava": "^6.4.1",
|
|
63
|
+
"xo": "^1.2.2"
|
|
64
|
+
},
|
|
65
|
+
"types": "./index.d.ts",
|
|
66
|
+
"main": "./dist-cjser/index.cjs",
|
|
67
|
+
"cjser": {
|
|
68
|
+
"sourceVersion": "7.0.1",
|
|
69
|
+
"cjserVersion": 2,
|
|
70
|
+
"original": {
|
|
71
|
+
"name": "filenamify",
|
|
72
|
+
"version": "7.0.1",
|
|
73
|
+
"exports": {
|
|
74
|
+
".": {
|
|
75
|
+
"types": "./index.d.ts",
|
|
76
|
+
"default": "./index.js"
|
|
77
|
+
},
|
|
78
|
+
"./browser": {
|
|
79
|
+
"types": "./filenamify.d.ts",
|
|
80
|
+
"default": "./filenamify.js"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"repository": "sindresorhus/filenamify",
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"filename-reserved-regex": "^4.0.0"
|
|
86
|
+
},
|
|
87
|
+
"files": [
|
|
88
|
+
"filenamify-path.d.ts",
|
|
89
|
+
"filenamify-path.js",
|
|
90
|
+
"filenamify.d.ts",
|
|
91
|
+
"filenamify.js",
|
|
92
|
+
"index.d.ts",
|
|
93
|
+
"index.js"
|
|
94
|
+
],
|
|
95
|
+
"scripts": {
|
|
96
|
+
"test": "xo && ava"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# filenamify
|
|
2
|
+
|
|
3
|
+
> Convert a string to a valid safe filename
|
|
4
|
+
|
|
5
|
+
On Unix-like systems, `/` is reserved. On Windows, [`<>:"/\|?*`](http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29#naming_conventions) along with trailing periods and spaces are reserved.
|
|
6
|
+
|
|
7
|
+
This module also removes non-printable control characters (including Unicode bidirectional marks) and normalizes Unicode whitespace.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install filenamify
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import filenamify from 'filenamify';
|
|
19
|
+
|
|
20
|
+
filenamify('<foo/bar>');
|
|
21
|
+
//=> '!foo!bar!'
|
|
22
|
+
|
|
23
|
+
filenamify('foo:"bar"', {replacement: '🐴'});
|
|
24
|
+
//=> 'foo🐴bar🐴'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
### filenamify(string, options?)
|
|
30
|
+
|
|
31
|
+
Convert a string to a valid filename.
|
|
32
|
+
|
|
33
|
+
### filenamifyPath(path, options?)
|
|
34
|
+
|
|
35
|
+
Convert the filename in a path to a valid filename and return the augmented path.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import {filenamifyPath} from 'filenamify';
|
|
39
|
+
|
|
40
|
+
filenamifyPath('foo:bar');
|
|
41
|
+
//=> 'foo!bar'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### options
|
|
45
|
+
|
|
46
|
+
Type: `object`
|
|
47
|
+
|
|
48
|
+
##### replacement
|
|
49
|
+
|
|
50
|
+
Type: `string`\
|
|
51
|
+
Default: `'!'`
|
|
52
|
+
|
|
53
|
+
String to use as replacement for reserved filename characters.
|
|
54
|
+
|
|
55
|
+
Cannot contain: `<` `>` `:` `"` `/` `\` `|` `?` `*` or control characters.
|
|
56
|
+
|
|
57
|
+
##### maxLength
|
|
58
|
+
|
|
59
|
+
Type: `number`\
|
|
60
|
+
Default: `100`
|
|
61
|
+
|
|
62
|
+
Truncate the filename to the given length.
|
|
63
|
+
|
|
64
|
+
Only the base of the filename is truncated, preserving the extension. If the extension itself is longer than `maxLength`, you will get a string that is longer than `maxLength`, so you need to check for that if you allow arbitrary extensions.
|
|
65
|
+
|
|
66
|
+
Truncation is grapheme-aware and will not split Unicode characters (surrogate pairs or extended grapheme clusters). If the remaining budget (after accounting for the extension) is smaller than a whole grapheme, the base filename may be truncated to an empty string to avoid splitting.
|
|
67
|
+
|
|
68
|
+
Systems generally allow up to 255 characters, but we default to 100 for usability reasons.
|
|
69
|
+
|
|
70
|
+
## Browser-only import
|
|
71
|
+
|
|
72
|
+
You can also import `filenamify/browser`, which only imports `filenamify` and not `filenamifyPath`, which relies on `path` being available or polyfilled. Importing `filenamify` this way is therefore useful when it is shipped using `webpack` or similar tools, and if `filenamifyPath` is not needed.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import filenamify from 'filenamify/browser';
|
|
76
|
+
|
|
77
|
+
filenamify('<foo/bar>');
|
|
78
|
+
//=> '!foo!bar!'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Related
|
|
82
|
+
|
|
83
|
+
- [filenamify-cli](https://github.com/sindresorhus/filenamify-cli) - CLI for this module
|
|
84
|
+
- [filenamify-url](https://github.com/sindresorhus/filenamify-url) - Convert a URL to a valid filename
|
|
85
|
+
- [valid-filename](https://github.com/sindresorhus/valid-filename) - Check if a string is a valid filename
|
|
86
|
+
- [unused-filename](https://github.com/sindresorhus/unused-filename) - Get a unused filename by appending a number if it exists
|
|
87
|
+
- [slugify](https://github.com/sindresorhus/slugify) - Slugify a string
|
|
88
|
+
|
|
89
|
+
## cjser
|
|
90
|
+
|
|
91
|
+
This package is a CommonJS-compatible build generated by cjser for projects that still need `require()` support. The source version matches the original npm package version, with a cjser prerelease suffix for this generated build.
|
|
92
|
+
Original repository: https://github.com/sindresorhus/filenamify
|