chanjs 2.7.3 → 2.7.5

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.
Files changed (93) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/Container.js +77 -0
  5. package/core/Controller.js +29 -0
  6. package/core/Database.js +93 -0
  7. package/core/Repository.js +327 -0
  8. package/core/Service.js +11 -0
  9. package/core/bootstrap/error-handler.js +104 -0
  10. package/core/bootstrap/hook-runner.js +64 -0
  11. package/core/bootstrap/middleware.js +35 -0
  12. package/core/bootstrap/router-loader.js +53 -0
  13. package/core/errors.js +224 -0
  14. package/core/loader.js +89 -0
  15. package/core/registry.js +17 -0
  16. package/doc/Cache.md +279 -106
  17. package/doc/Common.md +590 -134
  18. package/doc/Controller.md +166 -95
  19. package/doc/Help.md +299 -698
  20. package/doc/QuickStart.md +116 -0
  21. package/doc/Repository.md +560 -0
  22. package/doc/Service.md +201 -527
  23. package/index.js +75 -37
  24. package/middleware/body.js +17 -0
  25. package/middleware/cookie.js +7 -15
  26. package/middleware/cors.js +9 -27
  27. package/middleware/favicon.js +7 -17
  28. package/middleware/header.js +15 -16
  29. package/middleware/index.js +11 -11
  30. package/middleware/log.js +26 -56
  31. package/middleware/static.js +15 -28
  32. package/middleware/template.js +75 -115
  33. package/middleware/validate.js +79 -0
  34. package/middleware/waf.js +174 -197
  35. package/package.json +11 -3
  36. package/response/code.js +73 -0
  37. package/response/index.js +9 -6
  38. package/response/response.js +82 -236
  39. package/security/checker.js +26 -74
  40. package/security/index.js +4 -9
  41. package/security/jwt.js +69 -142
  42. package/security/keywords.js +32 -136
  43. package/security/rate-limit.js +38 -80
  44. package/security/sign.js +83 -176
  45. package/security/xss-filter.js +21 -53
  46. package/storage/cache.js +57 -196
  47. package/storage/index.js +3 -6
  48. package/storage/redis.js +123 -181
  49. package/storage/store.js +163 -188
  50. package/utils/data-parse.js +42 -186
  51. package/utils/file.js +73 -244
  52. package/utils/filter.js +22 -25
  53. package/utils/html.js +49 -33
  54. package/utils/index.js +21 -7
  55. package/utils/ip.js +31 -71
  56. package/utils/logger.js +117 -0
  57. package/utils/pages.js +55 -0
  58. package/utils/paths.js +18 -0
  59. package/utils/request.js +94 -136
  60. package/utils/signal.js +87 -0
  61. package/utils/time.js +33 -75
  62. package/utils/tree.js +112 -104
  63. package/App.js +0 -533
  64. package/base/Aop.js +0 -195
  65. package/base/Container.js +0 -161
  66. package/base/Controller.js +0 -65
  67. package/base/Database.js +0 -133
  68. package/base/Event.js +0 -61
  69. package/base/Repository.js +0 -644
  70. package/common/api.js +0 -35
  71. package/common/code.js +0 -52
  72. package/common/email.js +0 -191
  73. package/common/index.js +0 -5
  74. package/common/pages.js +0 -120
  75. package/common/utils.js +0 -73
  76. package/config/code.js +0 -166
  77. package/config/paths.js +0 -60
  78. package/doc/Aop.md +0 -269
  79. package/doc/Email.md +0 -114
  80. package/doc/Event.md +0 -232
  81. package/global/env.js +0 -11
  82. package/global/import.js +0 -39
  83. package/global/index.js +0 -8
  84. package/helper/index.js +0 -79
  85. package/loader/index.js +0 -6
  86. package/loader/loader.js +0 -138
  87. package/middleware/compress.js +0 -185
  88. package/middleware/setBody.js +0 -32
  89. package/realtime/index.js +0 -7
  90. package/realtime/sse.js +0 -424
  91. package/realtime/websocket.js +0 -540
  92. package/schedule/index.js +0 -6
  93. package/schedule/schedule.js +0 -491
package/utils/file.js CHANGED
@@ -1,284 +1,113 @@
1
- import path from "path";
2
- import fs from "fs";
3
-
4
- /**
5
- * 获取当前文件的目录路径
6
- * @param {string} importMetaUrl - import.meta.url 的值
7
- * @returns {string} 当前文件的目录路径
8
- * @description
9
- * 使用 import.meta.url 获取当前模块的 URL,然后提取目录路径
10
- * 适用于 ES 模块中获取 __dirname 的替代方案
11
- * @example
12
- * const __dirname = dirname(import.meta.url);
13
- * const configPath = path.join(__dirname, 'config.json');
14
- */
15
- export function dirname(importMetaUrl) {
16
- const url = new URL(importMetaUrl);
17
- return path.dirname(url.pathname);
18
- }
19
-
20
1
  /**
21
- * 检查路径是否包含目录穿越字符或绝对路径
22
- * @private
23
- * @param {string} inputPath - 待检查的路径
24
- * @returns {boolean} 是否安全
2
+ * 文件系统操作工具
3
+ * 提供安全的文件读写/删除/目录扫描功能(内置路径穿越防御)
25
4
  */
26
- function _isPathSafe(inputPath) {
27
- if (!inputPath || typeof inputPath !== 'string') return false;
28
- // 拒绝绝对路径(/etc/passwd)和 Windows 盘符(C:\)
29
- if (path.isAbsolute(inputPath) && !inputPath.startsWith(process.cwd())) return false;
30
- // 拒绝目录穿越
31
- const normalized = path.normalize(inputPath);
32
- if (normalized.includes('..')) return false;
33
- return true;
34
- }
5
+ import path from "path";
6
+ import fs from "fs";
7
+ import logger from "./logger.js";
35
8
 
36
9
  /**
37
- * 校验 fullPath 是否落在 process.cwd() 内
38
- * path.relative 避免 startsWith 前缀绕过(如 /project-evil 匹配 /project)
39
- * @private
40
- * @param {string} fullPath - 已解析的绝对路径
41
- * @returns {boolean} 是否在 cwd 内
10
+ * 路径安全校验:禁止越界项目根目录
11
+ * @param {string} inputPath
12
+ * @returns {string|null} 合法返回绝对路径,非法返回null
42
13
  */
43
- function _isWithinCwd(fullPath) {
14
+ export function safePath(inputPath) {
15
+ if (!inputPath || typeof inputPath !== "string") return null;
16
+ const fullPath = path.resolve(process.cwd(), inputPath);
44
17
  const rel = path.relative(process.cwd(), fullPath);
45
- return !rel.startsWith('..') && !path.isAbsolute(rel);
18
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
19
+ return fullPath;
46
20
  }
47
21
 
48
22
  /**
49
- * 删除图片文件
50
- * @param {string} filePath - 相对于项目根目录的文件路径
51
- * @returns {boolean} 如果文件存在并被删除返回 true,否则返回 false
52
- * @description
53
- * 删除指定路径的图片文件
54
- * 路径是相对于项目根目录(process.cwd())的
55
- *
56
- * 安全改进:
57
- * - 增加防御性路径校验,拒绝绝对路径和目录穿越
58
- * - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
59
- * - 业务层即使忘做校验也不会被利用删除任意文件
60
- * @example
61
- * const deleted = delImg('/uploads/avatar/123.jpg');
62
- * if (deleted) {
63
- * console.log('图片已删除');
64
- * }
23
+ * 删除文件
24
+ * @param {string} filePath 相对于项目根目录的文件路径
25
+ * @returns {boolean}
65
26
  */
66
27
  export function delImg(filePath) {
67
- if (!_isPathSafe(filePath)) {
68
- console.error('[delImg] 拒绝不安全路径:', filePath);
28
+ const fullPath = safePath(filePath);
29
+ if (!fullPath) {
30
+ logger.error("[delImg] 非法路径/路径越界:", filePath);
69
31
  return false;
70
32
  }
71
- const fullPath = path.resolve(process.cwd(), filePath);
72
- // 二次校验:解析后必须仍在 cwd 内(用 relative 避免 startsWith 前缀绕过)
73
- if (!_isWithinCwd(fullPath)) {
74
- console.error('[delImg] 路径越界:', filePath);
75
- return false;
76
- }
77
- if (fs.existsSync(fullPath)) {
78
- fs.unlinkSync(fullPath);
79
- return true;
80
- }
81
- return false;
82
- }
83
-
84
- /**
85
- * 获取文件树结构
86
- * @param {string} dirPath - 目录路径
87
- * @param {boolean} [recursive=true] - 是否递归获取子目录
88
- * @returns {Array} 文件树数组
89
- * @description
90
- * 递归读取目录下的所有文件和子目录,返回树形结构
91
- * @example
92
- * const tree = getFileTree('/path/to/directory');
93
- * // 返回: [{ name: 'file.html', path: '/path/to/directory/file.html', type: 'file' }, ...]
94
- */
95
- export function getFileTree(dirPath, recursive = true, basePath = null) {
96
- // 路径安全校验:拒绝不安全路径直接返回空数组
97
- if (!_isPathSafe(dirPath)) {
98
- console.error('[getFileTree] 拒绝不安全路径:', dirPath);
99
- return [];
100
- }
101
- if (!fs.existsSync(dirPath)) {
102
- return [];
103
- }
104
-
105
- const stats = fs.statSync(dirPath);
106
- if (!stats.isDirectory()) {
107
- return [];
108
- }
109
-
110
- // 二次校验:解析后必须仍在 cwd 内
111
- const fullPath = path.resolve(process.cwd(), dirPath);
112
- if (!_isWithinCwd(fullPath)) {
113
- console.error('[getFileTree] 路径越界:', dirPath);
114
- return [];
115
- }
116
-
117
- const items = [];
118
- const files = fs.readdirSync(dirPath);
119
-
120
- const base = basePath || process.cwd();
121
-
122
- for (const file of files) {
123
- const filePath = path.join(dirPath, file);
124
- const fileStats = fs.statSync(filePath);
125
-
126
- if (fileStats.isDirectory() && recursive) {
127
- const children = getFileTree(filePath, recursive, basePath);
128
- items.push({
129
- name: file,
130
- path: filePath,
131
- type: 'directory',
132
- children: children
133
- });
134
- } else if (fileStats.isFile()) {
135
- const relativePath = path.relative(base, filePath).replace(/\\/g, '/');
136
- items.push({
137
- name: file,
138
- path: filePath,
139
- relativePath: relativePath.startsWith('public') ? '/' + relativePath.replace(/^\//, '') : filePath,
140
- type: 'file',
141
- size: fileStats.size
142
- });
33
+ try {
34
+ if (fs.existsSync(fullPath)) {
35
+ fs.unlinkSync(fullPath);
36
+ return true;
143
37
  }
38
+ return false;
39
+ } catch (err) {
40
+ logger.error("[delImg] 删除文件异常", fullPath, err.message);
41
+ return false;
144
42
  }
145
-
146
- return items;
147
43
  }
148
44
 
149
45
  /**
150
- * 读取文件内容
151
- * @param {string} filePath - 文件路径
152
- * @returns {string} 文件内容
153
- * @description
154
- * 读取指定文件的内容
155
- *
156
- * 安全改进:
157
- * - 增加防御性路径校验,拒绝绝对路径和目录穿越
158
- * - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
159
- * - 与 saveFileContent 保持一致的安全策略
160
- * @example
161
- * const content = readFileContent('/path/to/file.html');
46
+ * 读取文本文件内容
47
+ * @param {string} filePath
48
+ * @returns {string} utf8 文件文本内容
49
+ * @throws 非法路径或文件不存在时抛异常
162
50
  */
163
51
  export function readFileContent(filePath) {
164
- if (!_isPathSafe(filePath)) {
165
- throw new Error(`[readFileContent] 拒绝不安全路径: ${filePath}`);
166
- }
167
- const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
168
- // 二次校验:解析后必须仍在 cwd 内(用 relative 避免 startsWith 前缀绕过)
169
- if (!_isWithinCwd(fullPath)) {
170
- throw new Error(`[readFileContent] 路径越界: ${filePath}`);
171
- }
172
- if (!fs.existsSync(fullPath)) {
173
- throw new Error('文件不存在');
174
- }
175
-
176
- const stats = fs.statSync(fullPath);
177
- if (!stats.isFile()) {
178
- throw new Error('路径不是文件');
179
- }
180
-
181
- return fs.readFileSync(fullPath, 'utf-8');
52
+ const fullPath = safePath(filePath);
53
+ if (!fullPath) throw new Error(`[readFileContent] 拒绝不安全/越界路径: ${filePath}`);
54
+ if (!fs.existsSync(fullPath)) throw new Error("文件不存在");
55
+ if (!fs.statSync(fullPath).isFile()) throw new Error("目标路径不是文件");
56
+ return fs.readFileSync(fullPath, "utf-8");
182
57
  }
183
58
 
184
59
  /**
185
- * 保存文件内容
186
- * @param {string} filePath - 文件路径
187
- * @param {string} content - 文件内容
188
- * @returns {void}
189
- * @description
190
- * 将内容保存到指定文件,如果目录不存在会自动创建
191
- *
192
- * 安全改进:
193
- * - 增加防御性路径校验,拒绝绝对路径和目录穿越
194
- * - 解析后最终路径必须落在 process.cwd() 内,否则拒绝
195
- * - 业务层即使忘做校验也不会被写入任意路径
196
- * @example
197
- * saveFileContent('/path/to/file.html', '<html>...</html>');
60
+ * 保存/覆写文件,不存在目录自动递归创建
61
+ * @param {string} filePath
62
+ * @param {string} content
63
+ * @throws 非法路径时抛异常
198
64
  */
199
65
  export function saveFileContent(filePath, content) {
200
- if (!_isPathSafe(filePath)) {
201
- throw new Error(`[saveFileContent] 拒绝不安全路径: ${filePath}`);
202
- }
203
- const fullPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
204
- // 二次校验:解析后必须仍在 cwd 内
205
- if (!_isWithinCwd(fullPath)) {
206
- throw new Error(`[saveFileContent] 路径越界: ${filePath}`);
207
- }
208
-
66
+ const fullPath = safePath(filePath);
67
+ if (!fullPath) throw new Error(`[saveFileContent] 拒绝不安全/越界路径: ${filePath}`);
209
68
  const dirPath = path.dirname(fullPath);
210
-
211
- if (!fs.existsSync(dirPath)) {
212
- fs.mkdirSync(dirPath, { recursive: true });
213
- }
214
-
215
- fs.writeFileSync(fullPath, content, 'utf-8');
69
+ if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
70
+ fs.writeFileSync(fullPath, content, "utf-8");
216
71
  }
217
72
 
218
73
  /**
219
- * 检查路径是否安全
220
- * @param {string} targetPath - 目标路径
221
- * @param {string} basePath - 基础路径
222
- * @returns {boolean} 路径是否安全
223
- * @description
224
- * 检查目标路径是否在基础路径范围内,防止路径遍历攻击
225
- * @example
226
- * const safe = isPathSafe('/project/file.html', '/project');
74
+ * 获取指定目录下一级文件夹名称列表(过滤隐藏目录)
75
+ * @param {string} folderPath
76
+ * @returns {string[]}
227
77
  */
228
- export function isPathSafe(targetPath, basePath) {
229
- const normalizedTarget = path.normalize(targetPath);
230
- const normalizedBase = path.normalize(basePath);
231
-
232
- if (normalizedTarget.includes('..')) {
233
- return false;
78
+ export function getFolders(folderPath) {
79
+ const fullPath = safePath(folderPath);
80
+ if (!fullPath) {
81
+ logger.error("[getFolders] 非法路径/路径越界:", folderPath);
82
+ return [];
83
+ }
84
+ try {
85
+ if (!fs.existsSync(fullPath)) return [];
86
+ return fs.readdirSync(fullPath)
87
+ .filter(name => !name.startsWith(".") && fs.statSync(path.join(fullPath, name)).isDirectory());
88
+ } catch (err) {
89
+ logger.error("[getFolders] 读取目录异常", fullPath, err.message);
90
+ return [];
234
91
  }
235
-
236
- const relativePath = path.relative(normalizedBase, normalizedTarget);
237
- return !relativePath.startsWith('..');
238
92
  }
239
93
 
240
94
  /**
241
- * 获取指定文件夹下的所有文件夹
242
- * @param {string} folderPath - 文件夹路径
243
- * @returns {Array<string>} 文件夹名称数组
244
- * @description
245
- * 同步获取指定文件夹下的所有文件夹(不包含隐藏文件)
246
- * @example
247
- * const folders = getFolders('/path/to/directory');
248
- * // 返回: ['default', 'test', ...]
95
+ * 同步获取目录下一级全部 .html 文件名称
96
+ * @param {string} folderPath
97
+ * @returns {string[]}
249
98
  */
250
- export function getFolders(folderPath) {
251
- // 路径安全校验:拒绝不安全路径直接返回空数组
252
- if (!_isPathSafe(folderPath)) {
253
- console.error('[getFolders] 拒绝不安全路径:', folderPath);
254
- return [];
255
- }
256
- if (!fs.existsSync(folderPath)) {
99
+ export function getHtmlFilesSync(folderPath) {
100
+ const fullPath = safePath(folderPath);
101
+ if (!fullPath) {
102
+ logger.error("[getHtmlFilesSync] 非法路径/路径越界:", folderPath);
257
103
  return [];
258
104
  }
259
- // 二次校验:解析后必须仍在 cwd 内
260
- const fullPath = path.resolve(process.cwd(), folderPath);
261
- if (!_isWithinCwd(fullPath)) {
262
- console.error('[getFolders] 路径越界:', folderPath);
105
+ try {
106
+ if (!fs.existsSync(fullPath)) return [];
107
+ return fs.readdirSync(fullPath)
108
+ .filter(name => fs.statSync(path.join(fullPath, name)).isFile() && path.extname(name).toLowerCase() === ".html");
109
+ } catch (err) {
110
+ logger.error("[getHtmlFilesSync] 读取目录异常", fullPath, err.message);
263
111
  return [];
264
112
  }
265
-
266
- const items = fs.readdirSync(folderPath);
267
- const folders = [];
268
-
269
- for (const item of items) {
270
- if (item.startsWith('.')) continue;
271
-
272
- const itemPath = path.join(folderPath, item);
273
- try {
274
- const stat = fs.statSync(itemPath);
275
- if (stat.isDirectory()) {
276
- folders.push(item);
277
- }
278
- } catch (err) {
279
- continue;
280
- }
281
- }
282
-
283
- return folders;
284
113
  }
package/utils/filter.js CHANGED
@@ -1,35 +1,32 @@
1
1
  /**
2
- * 从数组中的每个对象筛选指定字段
3
- * @param {Array<Object>} data - 要处理的对象数组
4
- * @param {Array<string>} fields - 需要保留的字段名数组
5
- * @returns {Array<Object>} 只包含指定字段的新对象数组
6
- * @description
7
- * 遍历数组中的每个对象,只保留指定的字段
8
- * 返回一个新的数组,不修改原始数据
2
+ * 从对象数组批量筛选指定字段,返回全新数组与全新对象,不修改原始入参数据
9
3
  * @example
10
- * const data = [
11
- * { id: 1, name: '张三', age: 25, email: 'test@example.com' },
12
- * { id: 2, name: '李四', age: 30, email: 'demo@example.com' }
13
- * ];
14
- * const result = filterFields(data, ['id', 'name']);
15
- * console.log(result);
16
- * // [
17
- * // { id: 1, name: '张三' },
18
- * // { id: 2, name: '李四' }
19
- * // ]
4
+ * filterFields([{ id: 1, name: '张三', age: 25 }], ['id', 'name'])
5
+ * // [{ id: 1, name: '张三' }]
6
+ * @param {Array<Object>} data 原始对象数组
7
+ * @param {string[]} fields 需要保留的字段名列表
8
+ * @returns {Array<Object>} 仅包含指定字段的新对象数组
20
9
  */
21
10
  export function filterFields(data, fields) {
22
- if (!Array.isArray(data) || data.length === 0) {
23
- return [];
24
- }
11
+ // 校验数组参数,非数组直接返回空数组
12
+ if (!Array.isArray(data)) return [];
13
+ // 字段列表非数组/空数组直接返回空对象数组
14
+ if (!Array.isArray(fields) || fields.length === 0) return data.map(() => ({}));
15
+
25
16
  return data.map((item) => {
26
- const filteredItem = {};
17
+ // 当前项不是普通对象,返回空对象规避取值报错
18
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
19
+ return {};
20
+ }
21
+ const filtered = {};
27
22
  for (const field of fields) {
28
- // 用原型上的方法调用,避免 item 重写 hasOwnProperty 导致原型污染
23
+ // 跳过非字符串字段名,避免异常key
24
+ if (typeof field !== "string") continue;
25
+ // 使用原型方法,防止item重写hasOwnProperty造成原型污染漏洞
29
26
  if (Object.prototype.hasOwnProperty.call(item, field)) {
30
- filteredItem[field] = item[field];
27
+ filtered[field] = item[field];
31
28
  }
32
29
  }
33
- return filteredItem;
30
+ return filtered;
34
31
  });
35
- }
32
+ }
package/utils/html.js CHANGED
@@ -1,5 +1,12 @@
1
1
  /**
2
- * 标准 HTML 编码(art-template {{}} 使用)
2
+ * HTML 处理工具
3
+ * 提供 HTML 实体编解码、script标签深度转义、HTML文本提取图片URL功能
4
+ */
5
+
6
+ /**
7
+ * 标准 HTML 实体编码(适配 art-template {{ }} 插值输出)
8
+ * @param {any} str 待转义文本
9
+ * @returns {string}
3
10
  */
4
11
  export function htmlEncode(str) {
5
12
  if (typeof str !== 'string') return '';
@@ -12,47 +19,56 @@ export function htmlEncode(str) {
12
19
  }
13
20
 
14
21
  /**
15
- * 【适配 art-template {{@}}】二次转义 script 标签
16
- * 必须这样写才能防 XSS!
22
+ * 深度危险标签二次转义(适配 art-template {{@ }} 原始输出防XSS)
23
+ * 双重转义 script/iframe/object/embed/form 标签
24
+ * @param {any} str 待处理HTML文本
25
+ * @returns {string}
17
26
  */
18
27
  export const escapeScript = (str) => {
19
28
  if (typeof str !== 'string') return '';
20
- return str
21
- // 二次转义 经过 {{@}} 解码后变成 &lt;script&gt;
22
- .replace(/<script\b[^>]*>/gi, '&amp;lt;script&amp;gt;')
23
- .replace(/<\/script>/gi, '&amp;lt;/script&amp;gt;');
29
+ // 一次性匹配所有危险开始/结束标签,统一替换为双重转义
30
+ return str.replace(/<\/?(script|iframe|object|embed|form)\b[^>]*>/gi, (match) => {
31
+ return match.replace(/</g, '&amp;lt;').replace(/>/g, '&amp;gt;');
32
+ });
24
33
  };
25
34
 
26
35
  /**
27
- * HTML 解码
28
- * 支持:命名实体(amp/lt/gt/quot/apos/nbsp)+ 十六进制数字实体(&#x...;)+ 十进制数字实体(&#...;)
36
+ * HTML 实体解码
37
+ * 支持:命名实体(amp/lt/gt/quot/apos/nbsp)、十进制数字实体、十六进制Unicode实体
38
+ * @param {any} str 带实体编码的字符串
39
+ * @returns {string}
29
40
  */
41
+ const NAMED_ENTITIES = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&nbsp;': ' ' };
42
+
30
43
  export function htmlDecode(str) {
31
44
  if (typeof str !== 'string') return '';
32
- const entities = {
33
- '&amp;': '&',
34
- '&lt;': '<',
35
- '&gt;': '>',
36
- '&quot;': '"',
37
- '&apos;': "'",
38
- '&nbsp;': ' ',
39
- };
40
45
  return str
41
- .replace(/&(amp|lt|gt|quot|apos|nbsp);/g, m => entities[m])
42
- // 十六进制数字实体:&#x3C; <(含 2/3/4/5/6 位 Unicode)
43
- .replace(/&#x([0-9a-f]+);/gi, (_, h) => {
44
- try {
45
- return String.fromCodePoint(parseInt(h, 16));
46
- } catch {
47
- return '';
48
- }
46
+ .replace(/&(amp|lt|gt|quot|apos|nbsp);/g, match => NAMED_ENTITIES[match])
47
+ .replace(/&#x([0-9a-f]+);/gi, (_, hexStr) => {
48
+ try { return String.fromCodePoint(parseInt(hexStr, 16)); }
49
+ catch { return ''; }
49
50
  })
50
- // 十进制数字实体:&#60; <
51
- .replace(/&#(\d+);/g, (_, d) => {
52
- try {
53
- return String.fromCodePoint(parseInt(d, 10));
54
- } catch {
55
- return '';
56
- }
51
+ .replace(/&#(\d+);/g, (_, decStr) => {
52
+ try { return String.fromCodePoint(parseInt(decStr, 10)); }
53
+ catch { return ''; }
57
54
  });
58
- }
55
+ }
56
+
57
+ /**
58
+ * 从HTML字符串中批量提取全部img标签src图片地址
59
+ * @param {string} str 富文本/HTML源码
60
+ * @returns {string[]}
61
+ */
62
+ export function filterImgFromStr(str) {
63
+ if (typeof str !== 'string' || str.trim() === '') return [];
64
+
65
+ const imgReg = /<img\b[^>]*?(?:>|\/>)/gi;
66
+ const srcReg = /src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
67
+
68
+ const tagList = str.match(imgReg);
69
+ if (!tagList) return [];
70
+
71
+ return tagList
72
+ .map(tag => tag.match(srcReg)?.[1]?.trim())
73
+ .filter(Boolean);
74
+ }
package/utils/index.js CHANGED
@@ -1,22 +1,33 @@
1
1
  /**
2
- * 通用工具模块 - 时间/文件/HTML/IP/数据解析/树/过滤
2
+ * 工具聚合层
3
+ *
4
+ * 业务侧使用方式:
5
+ * import { helper } from "chanjs";
6
+ * const { tree, getIp, formatDateFields } = helper;
7
+ *
8
+ * 也可直接从子模块导入(精确引用):
9
+ * import { getIp } from "chanjs/utils/ip.js";
3
10
  */
11
+
4
12
  // 时间处理
5
- export { formatTime, formatDateFields } from "./time.js";
13
+ export { formatDateFields } from "./time.js";
6
14
 
7
15
  // 文件操作
8
16
  export {
9
- dirname,
10
17
  delImg,
11
- getFileTree,
12
18
  readFileContent,
13
19
  saveFileContent,
14
- isPathSafe,
15
20
  getFolders,
21
+ getHtmlFilesSync,
16
22
  } from "./file.js";
17
23
 
18
24
  // HTML 处理
19
- export { htmlDecode, htmlEncode, escapeScript } from "./html.js";
25
+ export {
26
+ htmlEncode,
27
+ htmlDecode,
28
+ escapeScript,
29
+ filterImgFromStr,
30
+ } from "./html.js";
20
31
 
21
32
  // IP 获取
22
33
  export { getIp } from "./ip.js";
@@ -25,10 +36,13 @@ export { getIp } from "./ip.js";
25
36
  export { request } from "./request.js";
26
37
 
27
38
  // 数据解析
28
- export { dataParse, arrToObj, parseJsonFields, buildTree } from "./data-parse.js";
39
+ export { arrToObj, getChildrenId } from "./data-parse.js";
29
40
 
30
41
  // 树形结构
31
42
  export { tree, treeById } from "./tree.js";
32
43
 
33
44
  // 字段过滤
34
45
  export { filterFields } from "./filter.js";
46
+
47
+ // 分页工具
48
+ export { pages } from "./pages.js";