@yaohaixiao/renames.js 0.0.1
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/.editorconfig +9 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +3 -0
- package/.prettierignore +28 -0
- package/.prettierrc.js +41 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/bin/renames.js +415 -0
- package/commitlint.config.js +42 -0
- package/config/default.config.json +20 -0
- package/eslint.config.js +196 -0
- package/index.js +14 -0
- package/jest.config.js +51 -0
- package/jsconfig.json +9 -0
- package/lib/batch-rename.js +190 -0
- package/lib/create-config.js +265 -0
- package/lib/generate-filename.js +182 -0
- package/lib/read-list.js +87 -0
- package/lib/utils/get-basename.js +18 -0
- package/lib/utils/get-extension.js +20 -0
- package/lib/utils/is-empty-object.js +23 -0
- package/lib/utils/is-file-exists.js +26 -0
- package/lib/utils/is-function.js +29 -0
- package/lib/utils/pad-start.js +31 -0
- package/lib/utils/pad-zero.js +24 -0
- package/lib/utils/read-dir.js +73 -0
- package/lib/utils/read-file.js +56 -0
- package/lib/utils/remove-file.js +60 -0
- package/lib/utils/rename.js +74 -0
- package/lib/utils/replace-index-chapter.js +53 -0
- package/lib/utils/show-warning-log.js +20 -0
- package/lib/utils/sort-files.js +157 -0
- package/lib/utils/strip-non-digit.js +16 -0
- package/lib/utils/terminal-link.js +16 -0
- package/lib/utils/to-index-chapter.js +19 -0
- package/lib/utils/write-file.js +35 -0
- package/package.json +114 -0
- package/tests/batch-rename.spec.js +123 -0
- package/tests/get-basename.spec.js +23 -0
- package/tests/get-extension.spec.js +24 -0
- package/tests/is-empty-object.spec.js +73 -0
- package/tests/is-file-exsits.spec.js +17 -0
- package/tests/is-function.spec.js +63 -0
- package/tests/pad-start.spec.js +27 -0
- package/tests/pad-zero.spec.js +15 -0
- package/tests/read-dir.spec.js +42 -0
- package/tests/read-file.spec.js +52 -0
- package/tests/read-list.spec.js +91 -0
- package/tests/rename.spec.js +50 -0
- package/tests/replace-index-chapter.spec.js +40 -0
- package/tests/sort-files.spec.js +221 -0
- package/tests/strip-non-digit.spec.js +15 -0
- package/tests/to-index-chapter.spec.js +31 -0
- package/tests/write-file.spec.js +34 -0
package/jest.config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { supportsColor } from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* # Jest.config.js - jest 配置
|
|
5
|
+
*
|
|
6
|
+
* Created By: Yaohaixiao Update: 2026.1.10
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
moduleFileExtensions: ['js'],
|
|
10
|
+
// 处理导入映射
|
|
11
|
+
moduleNameMapper: {
|
|
12
|
+
'^#ansi-styles$': 'ansi-styles',
|
|
13
|
+
'^#supports-color$': 'supports-color',
|
|
14
|
+
'^@/(.*)$': '<rootDir>/$1',
|
|
15
|
+
},
|
|
16
|
+
testMatch: ['**/tests/*.spec.(js)'],
|
|
17
|
+
transform: {
|
|
18
|
+
'^.+\\.js$': [
|
|
19
|
+
'babel-jest',
|
|
20
|
+
{
|
|
21
|
+
presets: [
|
|
22
|
+
[
|
|
23
|
+
'@babel/preset-env',
|
|
24
|
+
{
|
|
25
|
+
targets: { node: 'current' },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
testEnvironment: 'node',
|
|
33
|
+
testTimeout: 5000,
|
|
34
|
+
// 确保不会忽略转换 chalk
|
|
35
|
+
transformIgnorePatterns: [
|
|
36
|
+
'node_modules/(?!(chalk|ansi-styles|supports-color)/)',
|
|
37
|
+
],
|
|
38
|
+
collectCoverage: true,
|
|
39
|
+
coverageDirectory: 'report/coverage',
|
|
40
|
+
reporters: [
|
|
41
|
+
'default',
|
|
42
|
+
[
|
|
43
|
+
'./node_modules/jest-html-reporter',
|
|
44
|
+
{
|
|
45
|
+
pageTitle: 'renames.js 单测报告',
|
|
46
|
+
outputPath: './report/unit-test/index.html',
|
|
47
|
+
includeFailureMsg: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
],
|
|
51
|
+
};
|
package/jsconfig.json
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
import generateFilename from './generate-filename.js';
|
|
7
|
+
import isFileExists from './utils/is-file-exists.js';
|
|
8
|
+
import isFunction from './utils/is-function.js';
|
|
9
|
+
import readList from './read-list.js';
|
|
10
|
+
import rename from './utils/rename.js';
|
|
11
|
+
import readDir from './utils/read-dir.js';
|
|
12
|
+
import showWarningLog from './utils/show-warning-log.js';
|
|
13
|
+
import sortFiles from './utils/sort-files.js';
|
|
14
|
+
|
|
15
|
+
// 辅助函数:规整配置项,统一处理默认值和衍生配置
|
|
16
|
+
const getNormalizedOptions = (options, filesCount) => {
|
|
17
|
+
const config = {
|
|
18
|
+
namesList: [],
|
|
19
|
+
prefix: '',
|
|
20
|
+
suffix: '',
|
|
21
|
+
connector: '',
|
|
22
|
+
autoIndex: false,
|
|
23
|
+
startIndex: 0,
|
|
24
|
+
indexPadZero: true,
|
|
25
|
+
indexPrefix: '第',
|
|
26
|
+
indexSuffix: '集',
|
|
27
|
+
delimiter: ':',
|
|
28
|
+
extname: '',
|
|
29
|
+
force: false,
|
|
30
|
+
filter: null,
|
|
31
|
+
sortBy: 'name',
|
|
32
|
+
order: 'desc',
|
|
33
|
+
sensitivity: 'base',
|
|
34
|
+
format: null,
|
|
35
|
+
...options,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 处理自动编号补零的长度配置
|
|
39
|
+
if (config.autoIndex && config.indexPadZero) {
|
|
40
|
+
config.indexLength = filesCount.toString().length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 处理过滤函数(确保为有效函数或 null)
|
|
44
|
+
config.filter = isFunction(config.filter) ? config.filter : null;
|
|
45
|
+
// 处理格式化函数(确保为有效函数或 null)
|
|
46
|
+
config.format = isFunction(config.format) ? config.format : null;
|
|
47
|
+
|
|
48
|
+
return config;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 辅助函数:加载并校验文件名列表,并将名称列表复制给 options
|
|
52
|
+
const analysisNamesList = (options, filesCount) => {
|
|
53
|
+
const { namesList } = options;
|
|
54
|
+
let names = [];
|
|
55
|
+
|
|
56
|
+
// 加载名称列表(数组直接使用,非数组调用 readList)
|
|
57
|
+
if (namesList) {
|
|
58
|
+
names =
|
|
59
|
+
Array.isArray(namesList) && namesList.length > 0
|
|
60
|
+
? namesList
|
|
61
|
+
: readList(namesList);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 校验名称列表与文件数量是否一致
|
|
65
|
+
const namesCount = names.length;
|
|
66
|
+
|
|
67
|
+
if (namesCount && namesCount !== filesCount) {
|
|
68
|
+
showWarningLog(
|
|
69
|
+
'警告',
|
|
70
|
+
`当前配置指定的 dirPath 文件夹中的文件数量`,
|
|
71
|
+
`与 namesList 中的数据数量不一致。`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 校验通过,挂载名称列表到配置并返回名称数组
|
|
76
|
+
options.names = names;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 辅助函数:处理单个文件的重命名逻辑
|
|
80
|
+
const doRename = (dirPath, oldFilename, index, config) => {
|
|
81
|
+
const { join } = path;
|
|
82
|
+
const oldFilePath = join(dirPath, oldFilename);
|
|
83
|
+
const stats = fs.statSync(oldFilePath);
|
|
84
|
+
|
|
85
|
+
// 过滤隐藏文件和文件夹(直接返回,不执行重命名)
|
|
86
|
+
if (stats.isDirectory() || oldFilename.startsWith('.')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 生成新文件名并执行重命名
|
|
91
|
+
const newFilename = generateFilename(oldFilename, index, config);
|
|
92
|
+
const newFilePath = join(dirPath, newFilename);
|
|
93
|
+
|
|
94
|
+
rename(oldFilePath, newFilePath, config.force || false);
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* # 批量重命名指定文件夹中文件的文件名
|
|
101
|
+
*
|
|
102
|
+
* @param {string} dirPath - 必须,目标文件夹(绝对或相对)路径
|
|
103
|
+
* @param {object} [options={}] - 可选,配置参数对象数据. Default is `{}`
|
|
104
|
+
* @param {Array | string} [options.namesList=[]] - 可选,要修改的对应的文件名列表数组或者文件名列表的数据.
|
|
105
|
+
* Default is `[]`
|
|
106
|
+
* @param {string} [options.prefix=''] - 可选,要添加的前缀. Default is `''`
|
|
107
|
+
* @param {string} [options.suffix=''] - 可选,要添加的后缀,添加在文件名和扩展名之间. Default is `''`
|
|
108
|
+
* @param {string} [options.connector=''] - 可选,prefix 和 suffix 之间的连接符. Default
|
|
109
|
+
* is `''`
|
|
110
|
+
* @param {boolean | string} [options.autoIndex=false] - 可选,是否自动编号. Default is
|
|
111
|
+
* `false`
|
|
112
|
+
* @param {number} [options.startIndex=0] - 可选,自动编号的起始索引值. Default is `0`
|
|
113
|
+
* @param {boolean} [options.indexPadZero=true] - 可选,是否索引编号自动补"0". Default is
|
|
114
|
+
* `true`
|
|
115
|
+
* @param {string} [options.indexPrefix='第'] - 可选,自动编号后缀. Default is `'第'`
|
|
116
|
+
* @param {string} [options.indexSuffix='集'] - 可选,是否自动编号. Default is `'集'`
|
|
117
|
+
* @param {string} [options.delimiter=':'] - 可选,自动编号和名字间的分隔符. Default is `':'`
|
|
118
|
+
* @param {boolean} [options.force=false] - 可选,是否强制重命名. Default is `false`
|
|
119
|
+
* @param {string} [options.extname=''] - 可选,重命名后的扩展名,例如: '.mp4'. Default is
|
|
120
|
+
* `''`
|
|
121
|
+
* @param {Function} [options.filter=null] - 可选,对给文件夹中的文件进行过滤的函数方法,返回过滤后的文件列表数据.
|
|
122
|
+
* Default is `null`
|
|
123
|
+
* @param {string | Function} [options.sortBy='name'] -
|
|
124
|
+
* 可选,排序方式,可选项:name、type、size、birthtime 和 modify-time),或者排序的功能函数. Default is
|
|
125
|
+
* `'name'`
|
|
126
|
+
* @param {string} [options.order='asc'] - 可选,文件名的排序方式(可选项:desc 和 asc). Default
|
|
127
|
+
* is `'asc'`
|
|
128
|
+
* @param {string} [options.sensitivity='base'] - 可选,name
|
|
129
|
+
* 排序时大小写/重音处理的方式,可选项:base、accent、case 和 variant. Default is `'base'`
|
|
130
|
+
* @param {Function} [options.format=null] - 可选,文件名的格式化方法. Default is `null`
|
|
131
|
+
* @returns {boolean} - 数据异常时返回 false,数据处理完成,返回 true.
|
|
132
|
+
*/
|
|
133
|
+
const batchRename = (dirPath, options = {}) => {
|
|
134
|
+
const { resolve } = path;
|
|
135
|
+
// 解析绝对路径(避免相对路径混乱)
|
|
136
|
+
const finalDirPath = resolve(dirPath);
|
|
137
|
+
|
|
138
|
+
// 检测文件路径是否存在
|
|
139
|
+
if (!isFileExists(finalDirPath)) {
|
|
140
|
+
showWarningLog('警告', finalDirPath, '文件夹不存在或已被删除。');
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 读取文件夹文件
|
|
145
|
+
const files = readDir(finalDirPath);
|
|
146
|
+
|
|
147
|
+
// 校验是否为空
|
|
148
|
+
if (files.length === 0) {
|
|
149
|
+
showWarningLog('警告', finalDirPath, '文件夹中没有任何文件。');
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 规整配置并过滤目标文件
|
|
154
|
+
const normalizedOptions = getNormalizedOptions(options, files.length);
|
|
155
|
+
const { filter, sortBy, order, sensitivity } = normalizedOptions;
|
|
156
|
+
const results = filter ? filter(files) : files;
|
|
157
|
+
|
|
158
|
+
if (results.length === 0) {
|
|
159
|
+
showWarningLog('警告', finalDirPath, '文件夹中没有符合过滤条件的文件。');
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 加载并校验文件名列表
|
|
164
|
+
analysisNamesList(normalizedOptions, results.length);
|
|
165
|
+
|
|
166
|
+
// 对目标文件进行排序
|
|
167
|
+
const sortedFiles = sortFiles(results, {
|
|
168
|
+
sortBy,
|
|
169
|
+
order,
|
|
170
|
+
sensitivity,
|
|
171
|
+
finalDirPath,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 执行批量重命名
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.greenBright('\n开始:'),
|
|
177
|
+
chalk.green('批量重命名进行中...\n'),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
for (const [index, oldFilename] of sortedFiles.entries()) {
|
|
181
|
+
doRename(finalDirPath, oldFilename, index, normalizedOptions);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(chalk.greenBright('\n结束:'), chalk.green('批量重命名完成!'));
|
|
185
|
+
|
|
186
|
+
// 正常执行完成返回 true
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export default batchRename;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { highlight } from 'cli-highlight';
|
|
7
|
+
import prettier from 'prettier';
|
|
8
|
+
|
|
9
|
+
import isFileExists from './utils/is-file-exists.js';
|
|
10
|
+
import readFile from './utils/read-file.js';
|
|
11
|
+
import writeFile from './utils/write-file.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* # 辅助函数:解析配置文件路径(统一路径处理,避免重复解构和 resolve)
|
|
15
|
+
*
|
|
16
|
+
* @function getConfigFilePaths
|
|
17
|
+
* @param {string} currentFilePath - 当前文件 URL 转换后的路径
|
|
18
|
+
* @returns {object} - 配置文件路径集合
|
|
19
|
+
*/
|
|
20
|
+
const getConfigFilePaths = (currentFilePath) => {
|
|
21
|
+
const configDir = path.dirname(currentFilePath);
|
|
22
|
+
const CONFIG_JSON_FILE = 'renames.config.js';
|
|
23
|
+
return {
|
|
24
|
+
CONFIG_JSON_FILE,
|
|
25
|
+
CONFIG_FILE_PATH: path.resolve(configDir, `../${CONFIG_JSON_FILE}`),
|
|
26
|
+
DEFAULT_CONFIG_PATH: path.resolve(
|
|
27
|
+
configDir,
|
|
28
|
+
'../config/default.config.json',
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* # 辅助函数:初始化默认配置(集中管理默认值,减少零散条件判断)
|
|
35
|
+
*
|
|
36
|
+
* @function initDefaultConfig
|
|
37
|
+
* @param {string} defaultConfigPath - 默认配置文件路径
|
|
38
|
+
* @returns {object} - 初始化后的配置对象
|
|
39
|
+
*/
|
|
40
|
+
const initDefaultConfig = (defaultConfigPath) => {
|
|
41
|
+
const defaultConfigJson = readFile(defaultConfigPath);
|
|
42
|
+
const baseConfig = defaultConfigJson ? JSON.parse(defaultConfigJson) : {};
|
|
43
|
+
// 补充核心默认值,统一覆盖
|
|
44
|
+
return {
|
|
45
|
+
dirPath: '',
|
|
46
|
+
namesList: '',
|
|
47
|
+
prefix: '',
|
|
48
|
+
suffix: '',
|
|
49
|
+
connector: '',
|
|
50
|
+
autoIndex: false,
|
|
51
|
+
startIndex: 0,
|
|
52
|
+
indexPadZero: true,
|
|
53
|
+
indexPrefix: '第',
|
|
54
|
+
indexSuffix: '集',
|
|
55
|
+
delimiter: ':',
|
|
56
|
+
force: false,
|
|
57
|
+
extname: '',
|
|
58
|
+
filter: null,
|
|
59
|
+
sortBy: 'name',
|
|
60
|
+
order: 'asc',
|
|
61
|
+
sensitivity: 'base',
|
|
62
|
+
format: null,
|
|
63
|
+
...baseConfig,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* # 辅助函数:赋值配置项(统一处理配置赋值,减少重复 if 判断)
|
|
69
|
+
*
|
|
70
|
+
* @function assignConfigKeys
|
|
71
|
+
* @param {object} config - 目标配置对象
|
|
72
|
+
* @param {object} options - 传入的选项
|
|
73
|
+
* @param {string[]} keys - 需要赋值的配置项键名列表
|
|
74
|
+
*/
|
|
75
|
+
const assignConfigKeys = (config, options, keys) => {
|
|
76
|
+
for (const key in keys) {
|
|
77
|
+
if (options[key] !== undefined) {
|
|
78
|
+
// 特殊处理:startIndex 转为数字类型
|
|
79
|
+
config[key] =
|
|
80
|
+
key === 'startIndex' ? Number.parseInt(options[key], 10) : options[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* # 辅助函数:交互式获取必填配置(提取重复交互逻辑,减少分支嵌套)
|
|
87
|
+
*
|
|
88
|
+
* @function getConfigInteractive
|
|
89
|
+
* @param {object} config - 目标配置对象
|
|
90
|
+
* @param {string} key - 配置项键名
|
|
91
|
+
* @param {string} promptText - 提示文本
|
|
92
|
+
* @param {string} demoText - 示例文本
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
const getConfigInteractive = async (config, key, promptText, demoText) => {
|
|
96
|
+
if (!config[key]) {
|
|
97
|
+
const answer = await input({
|
|
98
|
+
message: chalk.greenBright(
|
|
99
|
+
`可选,${promptText}(例如:"${demoText}"):`,
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
if (answer) config[key] = answer;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 批量重命名配置类型
|
|
108
|
+
*
|
|
109
|
+
* @typedef {object} RenameConfig
|
|
110
|
+
* @property {string} dirPath 目录路径
|
|
111
|
+
* @property {string} namesList 文件名列表(逗号分隔)
|
|
112
|
+
* @property {string} prefix 文件名前缀
|
|
113
|
+
* @property {string} suffix 文件名后缀
|
|
114
|
+
* @property {string} connector 前缀与索引/名称的连接符
|
|
115
|
+
* @property {boolean} autoIndex 是否自动添加索引
|
|
116
|
+
* @property {string} startIndex 起始索引
|
|
117
|
+
* @property {boolean} indexPadZero 索引是否补零
|
|
118
|
+
* @property {string} indexPrefix 索引前缀
|
|
119
|
+
* @property {string} indexSuffix 索引后缀
|
|
120
|
+
* @property {string} delimiter 列表分隔符
|
|
121
|
+
* @property {boolean} force 是否强制覆盖
|
|
122
|
+
* @property {string} extname 文件扩展名
|
|
123
|
+
* @property {string} sortBy 排序依据
|
|
124
|
+
* @property {string} order 排序顺序(asc/desc)
|
|
125
|
+
* @property {string} sensitivity 排序敏感度
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 取消操作类型
|
|
130
|
+
*
|
|
131
|
+
* @typedef {object} CancelAction
|
|
132
|
+
* @property {boolean} isCancel 是否取消操作
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* # 生成工具的配置文件
|
|
137
|
+
*
|
|
138
|
+
* @async
|
|
139
|
+
* @function createConfig
|
|
140
|
+
* @param {object} options - 配置信息
|
|
141
|
+
* @param {string} [options.dirPath=''] - 必须,目标文件夹路径(绝对路径或相对路径). Default is `''`
|
|
142
|
+
* @param {string} [options.namesList=''] -
|
|
143
|
+
* 可选,要修改的对应的文件名列表数组或者文件名列表的数据(例如:标题1,标题2). Default is `''`
|
|
144
|
+
* @param {string} [options.prefix=''] - 可选,要添加的前缀. Default is `''`
|
|
145
|
+
* @param {string} [options.suffix=''] - 可选,要添加的后缀,添加在文件名和扩展名之间. Default is `''`
|
|
146
|
+
* @param {string} [options.connector=''] - 可选,prefix 和 suffix 之间的连接符. Default
|
|
147
|
+
* is `''`
|
|
148
|
+
* @param {string} [options.autoIndex=false] - 可选,是否自动生成索引编号. Default is `false`
|
|
149
|
+
* @param {string} [options.startIndex=0] - 可选,自动编号的起始索引值. Default is `0`
|
|
150
|
+
* @param {string} [options.indexPadZero=false] - 可选,是否索引编号自动补‘0’. Default is
|
|
151
|
+
* `false`
|
|
152
|
+
* @param {string} [options.indexPrefix='第'] - 可选,自动编号后缀. Default is `'第'`
|
|
153
|
+
* @param {string} [options.indexSuffix='集'] - 可选,是否自动编号. Default is `'集'`
|
|
154
|
+
* @param {string} [options.delimiter=':'] - 可选,自动编号和名字间的分隔符. Default is `':'`
|
|
155
|
+
* @param {string} [options.force=false] - 可选,是否强制重命名. Default is `false`
|
|
156
|
+
* @param {string} [options.extname=''] - 可选,重命名后的扩展名,例如: '.mp4'. Default is
|
|
157
|
+
* `''`
|
|
158
|
+
* @param {string} [options.sortBy='name'] -
|
|
159
|
+
* 可选,文件名的排序类型(可选项:name、size、type、birthtime 和 modify-time). Default is
|
|
160
|
+
* `'name'`
|
|
161
|
+
* @param {string} [options.order='asc'] - 可选,文件名的排序方式(可选项:desc 和 asc). Default
|
|
162
|
+
* is `'asc'`
|
|
163
|
+
* @param {string} [options.sensitivity='base'] - 可选,name
|
|
164
|
+
* 排序时大小写/重音处理的方式,可选项:base、accent、case 和 variant. Default is `'base'`
|
|
165
|
+
* @returns {Promise<RenameConfig | CancelAction>}
|
|
166
|
+
*
|
|
167
|
+
* - 返回配置信息的 JSON 数据
|
|
168
|
+
*/
|
|
169
|
+
const createConfig = async (options) => {
|
|
170
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
171
|
+
const { CONFIG_JSON_FILE, CONFIG_FILE_PATH, DEFAULT_CONFIG_PATH } =
|
|
172
|
+
getConfigFilePaths(currentFilePath);
|
|
173
|
+
const DEMO_DIR_PATH = String.raw`C:\Downloads\Videos`;
|
|
174
|
+
const DEMO_LIST_PATH = String.raw`C:\Downloads\names.txt`;
|
|
175
|
+
|
|
176
|
+
// 初始化默认配置(集中管理,减少零散条件判断)
|
|
177
|
+
const config = initDefaultConfig(DEFAULT_CONFIG_PATH);
|
|
178
|
+
|
|
179
|
+
// 交互式确认是否生成/重写配置
|
|
180
|
+
const isConfigExists = isFileExists(CONFIG_FILE_PATH);
|
|
181
|
+
const confirmMessage = isConfigExists
|
|
182
|
+
? `配置文件 ${CONFIG_JSON_FILE} 已存在,是否重写配置?`
|
|
183
|
+
: `执行 init 命令后,工具将生成配置文件 ${CONFIG_JSON_FILE},是否继续?`;
|
|
184
|
+
const isConfirm = await confirm({ message: confirmMessage });
|
|
185
|
+
|
|
186
|
+
if (!isConfirm) {
|
|
187
|
+
return { isCancel: true };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 批量赋值可选配置项(提取工具函数,减少重复 if 判断)
|
|
191
|
+
const configKeys = [
|
|
192
|
+
'prefix',
|
|
193
|
+
'suffix',
|
|
194
|
+
'connector',
|
|
195
|
+
'autoIndex',
|
|
196
|
+
'startIndex',
|
|
197
|
+
'indexPadZero',
|
|
198
|
+
'indexPrefix',
|
|
199
|
+
'indexSuffix',
|
|
200
|
+
'delimiter',
|
|
201
|
+
'force',
|
|
202
|
+
'extname',
|
|
203
|
+
'sortBy',
|
|
204
|
+
'order',
|
|
205
|
+
'sensitivity',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// 优先使用传入的 options 的 dirPath 值
|
|
209
|
+
if (options.dirPath) {
|
|
210
|
+
config.dirPath = options.dirPath;
|
|
211
|
+
} else {
|
|
212
|
+
// 然后使用交互式获取 dirPath 参数
|
|
213
|
+
await getConfigInteractive(
|
|
214
|
+
config,
|
|
215
|
+
'dirPath',
|
|
216
|
+
'请输入需要执行重命名操作的文件夹路径',
|
|
217
|
+
DEMO_DIR_PATH,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 优先使用传入的 options 的 namesList 值
|
|
222
|
+
if (options.namesList) {
|
|
223
|
+
config.namesList = options.namesList;
|
|
224
|
+
} else {
|
|
225
|
+
// 然后使用交互式获取 namesList 参数
|
|
226
|
+
await getConfigInteractive(
|
|
227
|
+
config,
|
|
228
|
+
'namesList',
|
|
229
|
+
'请输入要修改的对应的文件名列表数组或者文件名列表的数据',
|
|
230
|
+
DEMO_LIST_PATH,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 其余非必填参数,如果用户输入了,按用户输入值,否则使用参数的默认值
|
|
235
|
+
assignConfigKeys(config, options, configKeys);
|
|
236
|
+
|
|
237
|
+
// 格式化配置并生成文件
|
|
238
|
+
const configCode = `export default ${JSON.stringify(config)}`;
|
|
239
|
+
const formatedConfigCode = await prettier.format(configCode, {
|
|
240
|
+
// JS 解析器
|
|
241
|
+
parser: 'babel',
|
|
242
|
+
// 缩进 2 空格
|
|
243
|
+
tabWidth: 2,
|
|
244
|
+
// 单引号
|
|
245
|
+
singleQuote: true,
|
|
246
|
+
// 尾逗号
|
|
247
|
+
trailingComma: 'es5',
|
|
248
|
+
// 保留分号
|
|
249
|
+
semi: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
console.log(
|
|
253
|
+
`\n配置文件内容如下:\n\n`,
|
|
254
|
+
highlight(formatedConfigCode, {
|
|
255
|
+
language: 'javascript',
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
writeFile(CONFIG_FILE_PATH, formatedConfigCode);
|
|
260
|
+
|
|
261
|
+
// 返回最终配置
|
|
262
|
+
return config;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export default createConfig;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import getBasename from './utils/get-basename.js';
|
|
2
|
+
import getExtension from './utils/get-extension.js';
|
|
3
|
+
import isFunction from './utils/is-function.js';
|
|
4
|
+
import padZero from './utils/pad-zero.js';
|
|
5
|
+
import toIndexChapter from './utils/to-index-chapter.js';
|
|
6
|
+
|
|
7
|
+
// 辅助函数:规整配置项,提供统一默认值
|
|
8
|
+
const getNormalizedOptions = (options) => ({
|
|
9
|
+
extname: '',
|
|
10
|
+
autoIndex: false,
|
|
11
|
+
indexLength: 2,
|
|
12
|
+
indexPrefix: '',
|
|
13
|
+
indexSuffix: '',
|
|
14
|
+
delimiter: ':',
|
|
15
|
+
prefix: '',
|
|
16
|
+
suffix: '',
|
|
17
|
+
connector: '',
|
|
18
|
+
names: [],
|
|
19
|
+
format: null,
|
|
20
|
+
startIndex: 0,
|
|
21
|
+
...options,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// 辅助函数:生成索引章节字符串
|
|
25
|
+
const generateIndexChapter = (index, options) => {
|
|
26
|
+
const {
|
|
27
|
+
autoIndex,
|
|
28
|
+
indexLength,
|
|
29
|
+
startIndex,
|
|
30
|
+
indexPrefix,
|
|
31
|
+
indexSuffix,
|
|
32
|
+
delimiter,
|
|
33
|
+
} = options;
|
|
34
|
+
if (!autoIndex) return '';
|
|
35
|
+
|
|
36
|
+
const onlyIndex = autoIndex === 'only';
|
|
37
|
+
const fileIndex = index + 1 + startIndex;
|
|
38
|
+
const paddedIndex = indexLength
|
|
39
|
+
? padZero(fileIndex, indexLength)
|
|
40
|
+
: `${fileIndex}`;
|
|
41
|
+
|
|
42
|
+
return toIndexChapter(paddedIndex, onlyIndex, {
|
|
43
|
+
indexPrefix,
|
|
44
|
+
indexSuffix,
|
|
45
|
+
delimiter,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 辅助函数:从名称列表生成核心文件名
|
|
50
|
+
const getFilenameByNames = (
|
|
51
|
+
oldFilename,
|
|
52
|
+
index,
|
|
53
|
+
indexChapter,
|
|
54
|
+
options,
|
|
55
|
+
namesList,
|
|
56
|
+
) => {
|
|
57
|
+
const { format, autoIndex } = options;
|
|
58
|
+
const onlyIndex = autoIndex === 'only';
|
|
59
|
+
const matchedName = namesList[index] || getBasename(oldFilename);
|
|
60
|
+
|
|
61
|
+
// 优先使用格式化函数,否则直接使用列表中的文件名
|
|
62
|
+
let newFilename = isFunction(format)
|
|
63
|
+
? format(oldFilename, matchedName, index, namesList)
|
|
64
|
+
: matchedName;
|
|
65
|
+
|
|
66
|
+
// 拼接自动编号(仅非 onlyIndex 模式)
|
|
67
|
+
if (autoIndex && !onlyIndex) {
|
|
68
|
+
newFilename = `${indexChapter}${newFilename}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// onlyIndex 模式直接返回索引章节
|
|
72
|
+
return onlyIndex ? indexChapter : newFilename;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 辅助函数:拼接前后缀
|
|
76
|
+
const appendPrefixAndSuffix = (filename, options) => {
|
|
77
|
+
const { prefix, suffix, connector } = options;
|
|
78
|
+
let result = filename;
|
|
79
|
+
|
|
80
|
+
// 拼接前缀
|
|
81
|
+
if (prefix) {
|
|
82
|
+
result = `${prefix}${connector}${result}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 拼接后缀
|
|
86
|
+
if (suffix) {
|
|
87
|
+
result = `${result}${connector}${suffix}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* # 辅助函数:获取格式化后的文件名
|
|
95
|
+
*
|
|
96
|
+
* @function formatBasename
|
|
97
|
+
* @param {string} basename - 文件的基础名称
|
|
98
|
+
* @param {Function} [format=null] - 用来格式化的回调函数. Default is `null`
|
|
99
|
+
* @param {number} [index=-1] - 文件在 dirPath 文件排序后的索引值. Default is `-1`
|
|
100
|
+
* @returns {string} - 返回格式化后的文件名
|
|
101
|
+
*/
|
|
102
|
+
const formatBasename = (basename, format = null, index = -1) => {
|
|
103
|
+
return isFunction(format) ? format(basename, index) : basename;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* # 获取文件名,通过原文件名,文件在文件夹中所有文件中的索引值和配置参数
|
|
108
|
+
*
|
|
109
|
+
* @function generateFilename
|
|
110
|
+
* @param {string} oldFilename - 原文件名
|
|
111
|
+
* @param {number} index - 文件在文件夹中所有文件中的索引值
|
|
112
|
+
* @param {object} [options={}] - 可选,配置参数对象数据. Default is `{}`
|
|
113
|
+
* @param {Array} [options.names=[]] - 可选,要修改的对应的文件名列表数组. Default is `[]`
|
|
114
|
+
* @param {string} [options.prefix=''] 可选,要添加的前缀. Default is `''`
|
|
115
|
+
* @param {string} [options.suffix=''] - 可选,要添加的后缀,添加在文件名和扩展名之间. Default is `''`
|
|
116
|
+
* @param {string} [options.connector=''] - 可选,Prefix 和 suffix 之间的连接符. Default
|
|
117
|
+
* is `''`
|
|
118
|
+
* @param {boolean | string} [options.autoIndex=false] - 可选,是否自动编号. Default is
|
|
119
|
+
* `false`
|
|
120
|
+
* @param {number} [options.startIndex=0] - 可选,自动编号的起始索引值. Default is `0`
|
|
121
|
+
* @param {boolean} [options.indexPadZero=true] - 可选,是否索引编号自动补"0". Default is
|
|
122
|
+
* `true`
|
|
123
|
+
* @param {number} [options.indexLength=2] - 可选,自动补齐的字符串长度值. Default is `2`
|
|
124
|
+
* @param {string} [options.indexPrefix='第'] - 可选,自动编号后缀. Default is `'第'`
|
|
125
|
+
* @param {string} [options.indexSuffix='集'] - 可选,是否自动编号. Default is `'集'`
|
|
126
|
+
* @param {string} [options.delimiter=':'] - 可选,自动编号和名字间的分隔符. Default is `':'`
|
|
127
|
+
* @param {Function | null} [options.format=null] - 可选,文件名的格式化方法. Default is
|
|
128
|
+
* `null`
|
|
129
|
+
* @param {string} [options.extname=''] - 可选,想替换的扩展名. Default is `''`
|
|
130
|
+
* @returns {string} - 返回生成的文件名字符串
|
|
131
|
+
*/
|
|
132
|
+
const generateFilename = (oldFilename, index, options = {}) => {
|
|
133
|
+
// 拆分文件名和扩展名(如 "photo.jpg" → 文件名"photo",扩展名".jpg")
|
|
134
|
+
const basename = getBasename(oldFilename);
|
|
135
|
+
const originExtname = getExtension(oldFilename);
|
|
136
|
+
|
|
137
|
+
// 获取配置信息
|
|
138
|
+
const normalizedOptions = getNormalizedOptions(options);
|
|
139
|
+
const { extname, names, format, autoIndex } = normalizedOptions;
|
|
140
|
+
const namesCount = names.length;
|
|
141
|
+
let matchedName = '';
|
|
142
|
+
|
|
143
|
+
// 直接使用 namesList 中的匹配的文件名,如果 namesList 和 dirPath 文件夹中的数量不一致,则使用原文件名
|
|
144
|
+
if (namesCount && !autoIndex) {
|
|
145
|
+
matchedName = names?.[index] ?? basename;
|
|
146
|
+
return `${formatBasename(matchedName, format)}${originExtname}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const namesList = Array.isArray(names) && names.length > 0 ? names : [];
|
|
150
|
+
const indexChapter = generateIndexChapter(index, normalizedOptions);
|
|
151
|
+
const onlyIndex = autoIndex === 'only';
|
|
152
|
+
let newFilename;
|
|
153
|
+
|
|
154
|
+
// 存在名称列表
|
|
155
|
+
if (namesList.length > 0) {
|
|
156
|
+
newFilename = getFilenameByNames(
|
|
157
|
+
oldFilename,
|
|
158
|
+
index,
|
|
159
|
+
indexChapter,
|
|
160
|
+
normalizedOptions,
|
|
161
|
+
namesList,
|
|
162
|
+
);
|
|
163
|
+
} else if (autoIndex) {
|
|
164
|
+
// 无名称列表,但开启自动编号
|
|
165
|
+
newFilename = onlyIndex
|
|
166
|
+
? indexChapter
|
|
167
|
+
: `${indexChapter}${formatBasename(basename, format, index)}`;
|
|
168
|
+
} else {
|
|
169
|
+
// 无名称列表,无自动编号(默认场景)
|
|
170
|
+
newFilename = formatBasename(basename, format, index);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 拼接前后缀 + 确定扩展名
|
|
174
|
+
newFilename = appendPrefixAndSuffix(newFilename, normalizedOptions);
|
|
175
|
+
|
|
176
|
+
const finalExtname = extname || originExtname;
|
|
177
|
+
|
|
178
|
+
// 返回最终结果
|
|
179
|
+
return `${newFilename}${finalExtname}`;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default generateFilename;
|