@codady/utils 0.0.38 → 0.0.40

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 (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/utils.cjs.js +576 -24
  3. package/dist/utils.cjs.min.js +3 -3
  4. package/dist/utils.esm.js +576 -24
  5. package/dist/utils.esm.min.js +3 -3
  6. package/dist/utils.umd.js +576 -24
  7. package/dist/utils.umd.min.js +3 -3
  8. package/dist.zip +0 -0
  9. package/examples/ajax-download.html +94 -0
  10. package/examples/ajax-get.html +59 -0
  11. package/examples/ajax-hook.html +55 -0
  12. package/examples/ajax-method.html +36 -0
  13. package/examples/ajax-post.html +37 -0
  14. package/examples/ajax-signal.html +91 -0
  15. package/examples/ajax-timeout.html +85 -0
  16. package/examples/buildUrl.html +99 -0
  17. package/examples/getUrlHash.html +71 -0
  18. package/examples/stringToEncodings-collision-test-registry.html +117 -0
  19. package/examples/stringToEncodings-collision-test.html +71 -0
  20. package/examples/stringToEncodings.html +138 -0
  21. package/examples/unicodeToEncodings.html +195 -0
  22. package/modules.js +17 -1
  23. package/modules.ts +17 -1
  24. package/package.json +1 -1
  25. package/src/ajax.js +380 -0
  26. package/src/ajax.ts +470 -0
  27. package/src/buildUrl.js +64 -0
  28. package/src/buildUrl.ts +86 -0
  29. package/src/capitalize.js +19 -0
  30. package/src/capitalize.ts +20 -0
  31. package/src/cleanQueryString.js +19 -0
  32. package/src/cleanQueryString.ts +20 -0
  33. package/src/getBodyHTML.js +53 -0
  34. package/src/getBodyHTML.ts +61 -0
  35. package/src/getEl.js +1 -1
  36. package/src/getEl.ts +6 -5
  37. package/src/getEls.js +1 -1
  38. package/src/getEls.ts +5 -5
  39. package/src/getUrlHash.js +37 -0
  40. package/src/getUrlHash.ts +39 -0
  41. package/src/isEmpty.js +24 -23
  42. package/src/isEmpty.ts +26 -23
  43. package/src/sliceStrEnd.js +63 -0
  44. package/src/sliceStrEnd.ts +60 -0
  45. package/src/stringToEncodings.js +56 -0
  46. package/src/stringToEncodings.ts +110 -0
  47. package/src/unicodeToEncodings.js +51 -0
  48. package/src/unicodeToEncodings.ts +55 -0
  49. package/src/arrayMutableMethods - /345/211/257/346/234/254.js" +0 -5
  50. package/src/comma - /345/211/257/346/234/254.js" +0 -2
  51. package/src/deepCloneToJSON - /345/211/257/346/234/254.js" +0 -47
  52. package/src/deepMergeMaps - /345/211/257/346/234/254.js" +0 -78
  53. package/src/escapeHTML - /345/211/257/346/234/254.js" +0 -29
  54. package/src/getDataType - /345/211/257/346/234/254.js" +0 -38
  55. package/src/isEmpty - /345/211/257/346/234/254.js" +0 -45
  56. package/src/mapMutableMethods - /345/211/257/346/234/254.js" +0 -5
  57. package/src/setMutableMethods - /345/211/257/346/234/254.js" +0 -5
  58. package/src/wrapMap - /345/211/257/346/234/254.js" +0 -119
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All changes to Utils including new features, updates, and removals are documented here.
4
4
 
5
+
6
+ ## [v0.0.40] - 2026-1-20
7
+
8
+ ### Distribution Files
9
+ * **JS**: https://unpkg.com/@codady/utils@0.0.40/dist/js/utils.js
10
+ * **Zip**:https://unpkg.com/@codady/utils@0.0.40/dist.zip
11
+
12
+ ### Changes
13
+
14
+ #### Fixed
15
+ * Null
16
+
17
+ #### Added
18
+ * Added the functions `stringToEncodings`、`unicodeToEncodings`.新增 `stringToEncodings`、`unicodeToEncodings`函数。
19
+
20
+ #### Removed
21
+ * Null
22
+
23
+
24
+ ## [v0.0.39] - 2026-1-20
25
+
26
+ ### Distribution Files
27
+ * **JS**: https://unpkg.com/@codady/utils@0.0.39/dist/js/utils.js
28
+ * **Zip**:https://unpkg.com/@codady/utils@0.0.39/dist.zip
29
+
30
+ ### Changes
31
+
32
+ #### Fixed
33
+ * Null
34
+
35
+ #### Added
36
+ * Added the functions `ajax`/`capitalize`/`buildUrl`/`cleanQueryString`/`getUrlHash`/`getBodyHTML`.新增 `ajax`/`capitalize`/`buildUrl`/`cleanQueryString`/`getUrlHash`/`getBodyHTML`函数。
37
+
38
+ #### Removed
39
+ * Null
40
+
41
+
5
42
  ## [v0.0.38] - 2026-1-16
6
43
 
7
44
  ### Distribution Files
package/dist/utils.cjs.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
- * @since Last modified: 2026-1-16 15:18:30
3
+ * @since Last modified: 2026-2-5 17:4:41
4
4
  * @name Utils for web front-end.
5
- * @version 0.0.38
5
+ * @version 0.0.40
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}
@@ -943,30 +943,31 @@ const isEmpty = (data) => {
943
943
  let type = getDataType(data), flag;
944
944
  if (!data) {
945
945
  //0,'',false,undefined,null
946
- flag = true;
946
+ return true;
947
947
  }
948
- else {
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;
948
+ if (['String', 'Number', 'Boolean'].includes(type)) {
949
+ return false;
969
950
  }
951
+ //function(){}|()=>{}
952
+ //[null]|[undefined]|['']|[""]
953
+ //[]|{}
954
+ //Symbol()|Symbol.for()
955
+ //Set,Map
956
+ //Date/Regex
957
+ flag = (type === 'Object') ? (Object.keys(data).length === 0) :
958
+ (type === 'Array') ? data.join('') === '' :
959
+ (type === 'Function') ? (data.toString().replace(/\s+/g, '').match(/{.*}/g)[0] === '{}') :
960
+ (type === 'Symbol') ? (data.toString().replace(/\s+/g, '').match(/\(.*\)/g)[0] === '()') :
961
+ (type === 'Set' || type === 'Map') ? data.size === 0 :
962
+ type === 'Date' ? isNaN(data.getTime()) :
963
+ type === 'RegExp' ? data.source === '' :
964
+ type === 'ArrayBuffer' ? data.byteLength === 0 :
965
+ (type === 'NodeList' || type === 'HTMLCollection') ? data.length === 0 :
966
+ ('length' in data && typeof data.length === 'number') ? data.length === 0 :
967
+ ('size' in data && typeof data.size === 'number') ? data.size === 0 :
968
+ (type === 'Error' || data instanceof Error) ? data.message === '' :
969
+ (type.includes('Array') && (['Uint8Array', 'Int8Array', 'Uint16Array', 'Int16Array', 'Uint32Array', 'Int32Array', 'Float32Array', 'Float64Array'].includes(type))) ? data.length === 0 :
970
+ false;
970
971
  return flag;
971
972
  };
972
973
 
@@ -1340,6 +1341,549 @@ const decodeHtmlEntities = (text) => {
1340
1341
  return textArea.value; // Get the decoded string from the text area
1341
1342
  };
1342
1343
 
1344
+ const getBodyHTML = (htmlText, selector) => {
1345
+ // Return early if the input is not a valid string or doesn't look like HTML
1346
+ if (!htmlText || typeof htmlText !== 'string') {
1347
+ return '';
1348
+ }
1349
+ try {
1350
+
1351
+ const parser = new DOMParser(),
1352
+
1353
+ doc = parser.parseFromString(htmlText, 'text/html'),
1354
+
1355
+ bodyContent = doc.body.innerHTML;
1356
+ if (selector) {
1357
+ // Normalize hash: ensure it's a valid ID selector
1358
+ const targetEl = doc.querySelector(selector);
1359
+ if (targetEl) {
1360
+ return targetEl.innerHTML;
1361
+ }
1362
+ // If hash is provided but element not found, we fallback to body or warn
1363
+ console.warn(`Element with selector "${selector}" not found in the HTML.`);
1364
+ }
1365
+ return bodyContent ? bodyContent.trim() : htmlText;
1366
+ }
1367
+ catch (error) {
1368
+
1369
+ console.error("Failed to parse HTML content using DOMParser:", error);
1370
+ return htmlText;
1371
+ }
1372
+ };
1373
+
1374
+ const getUrlHash = (url) => {
1375
+ // Return empty if input is null, undefined, or not a string
1376
+ if (!url || typeof url !== 'string') {
1377
+ return '';
1378
+ }
1379
+ try {
1380
+
1381
+ const baseUrl = window?.location?.origin || 'https://www.axui.cn', urlObj = new URL(url, baseUrl);
1382
+ return urlObj.hash;
1383
+ }
1384
+ catch (error) {
1385
+
1386
+ return '';
1387
+ }
1388
+ };
1389
+
1390
+ const cleanQueryString = (data) => {
1391
+ return typeof data === 'string' && (data.startsWith('?') || data.startsWith('&'))
1392
+ ? data.slice(1) // Remove the leading '?' or '&'
1393
+ : data; // Return the string as-is if no leading character is present
1394
+ };
1395
+
1396
+ const buildUrl = ({ url, data, cacheBustKey = '_t', appendCacheBust = true }) => {
1397
+ // 1. Extract and remove the hash (e.g., /page#section -> hash="#section")
1398
+ const hashIndex = url.indexOf('#');
1399
+ let hash = '', pureUrl = url;
1400
+ // If a hash exists, separate it from the base URL
1401
+ if (hashIndex !== -1) {
1402
+ hash = url.slice(hashIndex);
1403
+ pureUrl = url.slice(0, hashIndex);
1404
+ }
1405
+ // 2. Use the URL object to handle the base URL and existing query parameters.
1406
+ // `window.location.origin` ensures the support for relative paths (e.g., '/api/list').
1407
+ const urlObj = new URL(pureUrl, window.location.origin);
1408
+ // 3. Append business data (query parameters) to the URL if data is not empty
1409
+ if (!isEmpty(data)) {
1410
+ let params, dataType = getDataType(data);
1411
+ // If the data is a URLSearchParams object, directly use it
1412
+ if (dataType === 'URLSearchParams') {
1413
+ params = data;
1414
+ }
1415
+ else if (dataType === 'object') {
1416
+ // If the data is an object, convert it to URLSearchParams
1417
+ params = new URLSearchParams(data);
1418
+ }
1419
+ else {
1420
+ // If the data is a string, clean it up (remove leading '?' or '&')
1421
+ params = new URLSearchParams(cleanQueryString(data));
1422
+ }
1423
+ // Append new parameters to the existing URL search parameters
1424
+ params.forEach((value, key) => {
1425
+ urlObj.searchParams.append(key, value);
1426
+ });
1427
+ }
1428
+ // 4. Optionally add the cache-busting parameter if the flag is set
1429
+ appendCacheBust && cacheBustKey && urlObj.searchParams.set(cacheBustKey, Date.now().toString());
1430
+ // 5. Return the final URL: base URL + query parameters + original hash (if any)
1431
+ return urlObj.toString() + hash;
1432
+ };
1433
+
1434
+ const capitalize = (str) => {
1435
+ // Check if the input string is empty or undefined
1436
+ if (!str)
1437
+ return str;
1438
+ // Capitalize the first letter and return the new string
1439
+ return str.charAt(0).toUpperCase() + str.slice(1);
1440
+ };
1441
+
1442
+ const ajax = (options) => {
1443
+ // Validation
1444
+ if (isEmpty(options)) {
1445
+ return Promise.reject(new Error('Options are required'));
1446
+ }
1447
+ if (!options.url || typeof options.url !== 'string') {
1448
+ return Promise.reject(new Error('URL is required and must be a string'));
1449
+ }
1450
+ // Default configuration
1451
+ const config = {
1452
+ url: '',
1453
+ method: 'POST',
1454
+ async: true,
1455
+ selector: '',
1456
+ data: null,
1457
+ timeout: 3600000,
1458
+ headers: {},
1459
+ responseType: '',
1460
+ catchError: false,
1461
+ signal: null,
1462
+ xhrFields: {},
1463
+ cacheBustKey: '_t',
1464
+ precision: 2,
1465
+ //
1466
+ onAbort: null,
1467
+ onTimeout: null,
1468
+ //
1469
+ onBeforeSend: null,
1470
+ //
1471
+ onCreated: null,
1472
+ onOpened: null,
1473
+ onHeadersReceived: null,
1474
+ onLoading: null,
1475
+ //
1476
+ onSuccess: null,
1477
+ onFailure: null,
1478
+ onInformation: null,
1479
+ onRedirection: null,
1480
+ onClientError: null,
1481
+ onServerError: null,
1482
+ onUnknownError: null,
1483
+ onError: null,
1484
+ onFinish: null,
1485
+ //
1486
+ onDownload: null,
1487
+ onUpload: null,
1488
+ onComplete: null,
1489
+ };
1490
+ //合并参数
1491
+ Object.assign(config, options);
1492
+ //
1493
+ const method = config.method.toUpperCase() || 'POST', methodsWithoutBody = ['GET', 'HEAD', 'TRACE'];
1494
+ //创建XMLHttpRequest
1495
+ let xhr = new XMLHttpRequest(),
1496
+ //设置发送数据和预设请求头
1497
+ requestData = null, headerContentType = config?.headers?.['Content-Type'] || config?.headers?.['content-type'], removeHeader = () => {
1498
+ if (headerContentType) {
1499
+ delete config.headers['Content-Type'];
1500
+ delete config.headers['content-type'];
1501
+ }
1502
+ };
1503
+ if (!isEmpty(config.data)) {
1504
+ let dataType = getDataType(config.data);
1505
+ if (dataType === 'FormData') {
1506
+ //如果是new FormData格式,直接相等
1507
+ requestData = config.data;
1508
+ // 不需要手动设置Content-Type,浏览器会自动设置
1509
+ //config.contType = 'multipart/form-data';
1510
+ removeHeader();
1511
+ }
1512
+ else if (dataType === 'Object') {
1513
+ //如果是对象格式{name:'',age:''}
1514
+ //并且此时已经设置了contType
1515
+ if (!headerContentType) {
1516
+ //如果未设置则默认设为如下contType
1517
+ //Content-Type=application/x-www-form-urlencoded
1518
+
1519
+ requestData = new URLSearchParams(config.data).toString();
1520
+ //URLSearchParams.toString => `a=1&b=3`
1521
+ //非get、head方法修正content-type
1522
+ if (!methodsWithoutBody.includes(method)) {
1523
+ config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1524
+ }
1525
+ }
1526
+ else if (headerContentType?.includes('application/json')) {
1527
+ //Content-Type=application/json或contentType=application/json
1528
+ requestData = JSON.stringify(config.data);
1529
+ }
1530
+ else {
1531
+ requestData = config.data;
1532
+ }
1533
+ }
1534
+ else if (dataType === 'String') {
1535
+ //未设置或,已经设置了Content-Type=application/x-www-form-urlencoded
1536
+ if (!headerContentType || headerContentType.includes('urlencoded')) {
1537
+ //如果是name=''&age=''字符串
1538
+ //?name=''&age=''或&name=''&age=''统一去掉第一个&/?
1539
+ requestData = cleanQueryString(config.data.trim());
1540
+ //非get、head方法修正content-type
1541
+ if (!methodsWithoutBody.includes(method) && !headerContentType) {
1542
+ config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
1543
+ }
1544
+ }
1545
+ else {
1546
+ requestData = config.data;
1547
+ }
1548
+ }
1549
+ else {
1550
+ requestData = config.data;
1551
+ }
1552
+ }
1553
+ //设置超时时间
1554
+ xhr.timeout = config.timeout;
1555
+ // 响应类型
1556
+ if (config.responseType) {
1557
+ xhr.responseType = config.responseType;
1558
+ }
1559
+ //返回promise
1560
+ const result = new Promise((resolve, reject) => {
1561
+ //超时监听
1562
+ const timeoutHandler = () => {
1563
+ cleanup();
1564
+ let resp = { ...context, status: xhr.status, content: xhr.response, type: 'timeout' };
1565
+ //回调,status和content在此确认
1566
+ config?.onTimeout?.(resp);
1567
+ //reject只能接受一个参数
1568
+ config.catchError ? reject(resp) : resolve(resp);
1569
+ //超时也是不能获得数据的行为,定义为failure
1570
+ config?.onFailure?.(resp);
1571
+ //timeout会经过onreadystatechange,但是被及时的return了,所以这里多加一行
1572
+ config?.onFinish?.(resp);
1573
+ },
1574
+ //报错监听
1575
+ errorHandler = (resp) => {
1576
+ //这几个错误来自xhr.onreadystatechange
1577
+ if (resp.type === 'client-error') {
1578
+ config?.onClientError?.({ ...context });
1579
+ }
1580
+ else if (resp.type === 'server-error') {
1581
+ config?.onServerError?.({ ...context });
1582
+ }
1583
+ else if (resp.type === 'unknown-error') {
1584
+ config?.onUnknownError?.({ ...context });
1585
+ }
1586
+ //此外还会有xhr.onerror的错误,所以需要统一使用onError监听
1587
+ config?.onError?.(resp);
1588
+ //reject只能接受一个参数
1589
+ config.catchError ? reject(resp) : resolve(resp);
1590
+ },
1591
+ //取消监听
1592
+ abortHandler = () => {
1593
+ cleanup();
1594
+ const resp = { ...context, status: xhr.status, type: 'abort' };
1595
+ config.catchError ? reject(resp) : resolve(resp);
1596
+ //回调,status和content在此确认
1597
+ config?.onAbort?.(resp);
1598
+ //abort行为不会经过onreadystatechange,这里需要多这一行以表示xhr的完成(结束)
1599
+ config?.onFinish?.(resp);
1600
+ }, abortHandlerWithSignal = () => {
1601
+ //先中止请求,防止触发其他 readystate 事件
1602
+ xhr.abort();
1603
+ abortHandler();
1604
+ },
1605
+ //成功监听
1606
+ successHandler = (resp) => {
1607
+ //成功回调
1608
+ config?.onSuccess?.(resp);
1609
+ //resolve只能接受一个参数
1610
+ resolve(resp);
1611
+ },
1612
+ //统一处理abort
1613
+ cleanup = () => {
1614
+ // 如果使用了AbortSignal,则移除它的事件监听器
1615
+ config.signal && config.signal.removeEventListener('abort', abortHandlerWithSignal);
1616
+ // 移除各类事件监听器
1617
+ config.onError && xhr.removeEventListener('error', errorHandler);
1618
+ config.onTimeout && xhr.removeEventListener('timeout', timeoutHandler);
1619
+ // 解绑上传/下载进度事件
1620
+ config.onUpload && xhr.upload.removeEventListener('progress', uploadProgressHandler);
1621
+ config.onDownload && xhr.removeEventListener('progress', downloadProgressHandler);
1622
+ //销毁
1623
+ xhr.onreadystatechange = null;
1624
+ },
1625
+ // Context object to track state
1626
+ context = {
1627
+ //原始xhr
1628
+ xhr,
1629
+ //发送的数据
1630
+ data: requestData,
1631
+ //可取消的函数
1632
+ abort: abortHandler,
1633
+ //xhr.status
1634
+ status: '',
1635
+ //响应的内容
1636
+ content: null,
1637
+ //0~4阶段编号
1638
+ stage: 0,
1639
+ //阶段名称
1640
+ type: 'unset',
1641
+ //上传和下载进度
1642
+ progress: {}
1643
+ }, getProgressValues = (ratio) => {
1644
+ let text = (ratio * 100).toFixed(config.precision);
1645
+ return { percent: parseFloat(text), text };
1646
+ },
1647
+ //定义进度函数
1648
+ progressHandler = (name, data, callback) => {
1649
+ if (data.lengthComputable) {
1650
+ const resp = { ...context, status: xhr.status }, ratio = data.loaded / data.total, { percent, text } = getProgressValues(ratio);
1651
+ resp.progress = {
1652
+ name,
1653
+ loaded: data.loaded,
1654
+ total: data.total,
1655
+ timestamp: (new Date(data.timeStamp)).getTime(),
1656
+ ratio,
1657
+ percent,
1658
+ text,
1659
+ };
1660
+ callback?.(resp);
1661
+ if (ratio >= 1) {
1662
+ Object.assign(resp.progress, getProgressValues(1));
1663
+ config?.onComplete?.(resp);
1664
+ }
1665
+ }
1666
+ }, uploadProgressHandler = (data) => {
1667
+ progressHandler('upload', data, (resp) => config.onUpload(resp));
1668
+ }, downloadProgressHandler = (data) => {
1669
+ progressHandler('download', data, (resp) => config.onDownload(resp));
1670
+ };
1671
+ //使用AbortSignal
1672
+ if (config.signal) {
1673
+ if (config.signal.aborted)
1674
+ return abortHandlerWithSignal();
1675
+ config.signal.addEventListener('abort', abortHandlerWithSignal);
1676
+ }
1677
+ //监听上传进度
1678
+ config.onUpload && xhr.upload.addEventListener('progress', uploadProgressHandler);
1679
+ //监听下载进度
1680
+ config.onDownload && xhr.addEventListener('progress', downloadProgressHandler);
1681
+ // 事件监听器
1682
+ config.onError && xhr.addEventListener('error', errorHandler);
1683
+ config.onTimeout && xhr.addEventListener('timeout', timeoutHandler);
1684
+ config.onAbort && xhr.addEventListener('abort', abortHandler);
1685
+ // 手动触发 Created 状态
1686
+ config.onCreated?.({ ...context, type: 'created' });
1687
+ //状态判断
1688
+ xhr.onreadystatechange = function () {
1689
+ context.stage = xhr.readyState;
1690
+ context.status = xhr.status;
1691
+ const statusMap = { 1: 'opened', 2: 'headersReceived', 3: 'loading' };
1692
+ //0=created放在外侧确保能触发,如果放在.onreadystatechange可能触发不了
1693
+ if (xhr.readyState < 4) {
1694
+ if (!xhr.readyState)
1695
+ return;
1696
+ context.type = statusMap[xhr.readyState];
1697
+ config[`on${capitalize(context.type)}`]?.({ ...context });
1698
+ return;
1699
+ }
1700
+ //tiemeout事件也会执行这里,此时需要让它触发onTimeout事件
1701
+ //abort和timeout行为的status是0
1702
+ //不过abort行为不会执行到这里
1703
+ if (xhr.status === 0 && context.type !== 'abort') {
1704
+ return;
1705
+ }
1706
+ //已经请求成功,不会有timeout事件,也不需要abort了,所以移除abort事件
1707
+ cleanup();
1708
+ //根据状态码判断响应结果
1709
+ 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;
1710
+ //已经获得返回数据
1711
+ if (isSuccess) {
1712
+ if (!config.responseType || xhr.responseType === 'text') {
1713
+ //可能返回字符串类型的对象,wordpress的REST API
1714
+ let trim = xhr.responseText.trim(), content = '';
1715
+ if ((trim.startsWith('[') && trim.endsWith(']')) || (trim.startsWith('{') && trim.endsWith('}'))) {
1716
+ //通过判断开头字符是{或[来确定异步页面是否是JSON内容,如果是则转成JSON对象
1717
+ try {
1718
+ content = JSON.parse(trim);
1719
+ }
1720
+ catch {
1721
+ console.warn('Malformed JSON detected, falling back to text.');
1722
+ content = xhr.responseText;
1723
+ }
1724
+ }
1725
+ else if (/(<\/html>|<\/body>)/i.test(trim)) {
1726
+ //请求了一个HTML页面
1727
+ //返回文本类型DOMstring
1728
+ let urlHash = getUrlHash(config.url);
1729
+ content = getBodyHTML(trim, config.selector || urlHash);
1730
+ }
1731
+ else {
1732
+ //普通文本,不做任何处理
1733
+ content = xhr.responseText;
1734
+ }
1735
+ //content=文本字符串/json
1736
+ context.content = content;
1737
+ }
1738
+ else {
1739
+ //content=json、blob、document、arraybuffer等类型,如果知道服务器返回的XML, xhr.responseType应该为document
1740
+ context.content = xhr.response;
1741
+ }
1742
+ context.type = 'success';
1743
+ successHandler({ ...context });
1744
+ }
1745
+ else {
1746
+ //失败回调
1747
+ context.content = xhr.response;
1748
+ context.type = isInformation ? 'infomation' : isRedirection ? 'redirection' : isClientError ? 'client-error' : isServerError ? 'server-error' : 'unknown-error';
1749
+ //
1750
+ if (isInformation) {
1751
+ config?.onInformation?.({ ...context });
1752
+ }
1753
+ else if (isRedirection) {
1754
+ config?.onRedirection?.({ ...context });
1755
+ }
1756
+ else {
1757
+ errorHandler({ ...context });
1758
+ }
1759
+ //
1760
+ config?.onFailure?.({ ...context });
1761
+ }
1762
+ config?.onFinish?.({ ...context });
1763
+ };
1764
+ //发送异步请求
1765
+ let openParams = [method, config.url, config.async];
1766
+ if (methodsWithoutBody.includes(method)) {
1767
+ // 拼接url => xxx.com?a=0&b=1#hello
1768
+ const url = buildUrl({
1769
+ url: config.url,
1770
+ data: requestData,
1771
+ cacheBustKey: config.cacheBustKey,
1772
+ appendCacheBust: true,
1773
+ });
1774
+ openParams = [method, url, config.async];
1775
+ }
1776
+ //设置xhr其他字段
1777
+ for (let k in config.xhrFields) {
1778
+ config.xhrFields.hasOwnProperty(k) && (xhr[k] = config.xhrFields[k]);
1779
+ }
1780
+ //与服务器建立连接
1781
+ xhr.open(...openParams);
1782
+ //有则设置,仅跳过空内容
1783
+ for (let k in config.headers) {
1784
+ config.headers.hasOwnProperty(k) && !isEmpty(config.headers[k]) && xhr.setRequestHeader(k, config.headers[k]);
1785
+ }
1786
+ config?.onBeforeSend?.(({ ...context, status: xhr.status, type: 'beforeSend' }));
1787
+ //发送请求,get和head不需要发送数据
1788
+ xhr.send(methodsWithoutBody.includes(method) ? null : (requestData || null));
1789
+ //open和send阶段已经是异步了,无法使用try+catch捕获错误
1790
+ });
1791
+ //绑定xhr和abort
1792
+ result.xhr = xhr;
1793
+ result.abort = () => xhr.abort();
1794
+ return result;
1795
+ };
1796
+ // Static Helper Methods
1797
+ //get、head、trace是不需要发送数据的,data将被转为url参数处理
1798
+ ['post', 'put', 'delete', 'patch', 'options', 'get', 'head', 'trace'].forEach(method => {
1799
+ ajax[method] = (url, data, options = { url: '' }) => ajax({ ...options, method, url, data });
1800
+ });
1801
+ ajax.all = (requests) => Promise.all(requests.map(ajax));
1802
+
1803
+ const stringToEncodings = (name, options = {}) => {
1804
+ // Default: Supplementary Private Use Area (Plane 15 and Plane 16)
1805
+ //1,114,110 places,5000 strings => 0 collision
1806
+ const start = options.start ?? 0xF0000, end = options.end ?? 0x10FFFD, range = BigInt(end - start + 1), registry = options.registryMap,
1807
+
1808
+ formatResult = (name, codePoint, hash, collision) => {
1809
+ const hex = codePoint.toString(16).toUpperCase();
1810
+ return {
1811
+ name,
1812
+ unicode: `U+${hex}`,
1813
+ htmlDec: `&#${codePoint};`,
1814
+ htmlHex: `&#x${hex};`,
1815
+ hex,
1816
+ codePoint,
1817
+ hash,
1818
+ collision,
1819
+ };
1820
+ };
1821
+ // -----------------------------
1822
+ // 1. Compute FNV-1a 64-bit hash
1823
+ // -----------------------------
1824
+ let hash = BigInt("0xcbf29ce484222325");
1825
+ const prime = BigInt("0x100000001b3");
1826
+ for (const ch of name) {
1827
+ hash ^= BigInt(ch.codePointAt(0));
1828
+ hash *= prime;
1829
+ }
1830
+ const hashHex = hash.toString(16).toUpperCase();
1831
+ // -----------------------------
1832
+ // 2. Stateless mode (no registry)
1833
+ // -----------------------------
1834
+ if (!registry) {
1835
+ const offset = Number(hash % range), codePoint = start + offset;
1836
+ return formatResult(name, codePoint, hashHex, false);
1837
+ }
1838
+ // -----------------------------
1839
+ // 3. Registry mode (0 collision)
1840
+ // -----------------------------
1841
+ // Already assigned → return stable mapping
1842
+ if (registry.has(name)) {
1843
+ return formatResult(name, registry.get(name), hashHex, false);
1844
+ }
1845
+ // Initial candidate from hash
1846
+ let offset = Number(hash % range), codePoint = start + offset, collision = false;
1847
+ const used = new Set(registry.values());
1848
+ // Linear probing to resolve collisions
1849
+ while (used.has(codePoint)) {
1850
+ collision = true;
1851
+ offset = (offset + 1) % Number(range);
1852
+ codePoint = start + offset;
1853
+ }
1854
+ // Commit allocation
1855
+ registry.set(name, codePoint);
1856
+ return formatResult(name, codePoint, hashHex, collision);
1857
+ };
1858
+
1859
+ const unicodeToEncodings = (input) => {
1860
+ let codePoint;
1861
+ if (typeof input === "number") {
1862
+ codePoint = input;
1863
+ }
1864
+ else {
1865
+ const cleaned = input.trim()
1866
+ .replace(/^U\+/i, "")
1867
+ .replace(/^0x/i, "");
1868
+ codePoint = /^[0-9A-F]+$/i.test(cleaned)
1869
+ ? parseInt(cleaned, 16)
1870
+ : parseInt(cleaned, 10);
1871
+ }
1872
+ // Validate parsed code point
1873
+ if (!Number.isFinite(codePoint)) {
1874
+ throw new Error("Invalid Unicode input");
1875
+ }
1876
+ // Convert code point to uppercase hexadecimal representation
1877
+ const hex = codePoint.toString(16).toUpperCase();
1878
+ return {
1879
+ unicode: `U+${hex}`,
1880
+ hex,
1881
+ codePoint,
1882
+ htmlDec: `&#${codePoint};`,
1883
+ htmlHex: `&#x${hex};`,
1884
+ };
1885
+ };
1886
+
1343
1887
  const utils = {
1344
1888
  //executeStr,
1345
1889
  getDataType,
@@ -1383,6 +1927,14 @@ const utils = {
1383
1927
  escapeHTML,
1384
1928
  toSingleLine,
1385
1929
  renderTpl,
1930
+ getBodyHTML,
1931
+ getUrlHash,
1932
+ buildUrl,
1933
+ ajax,
1934
+ capitalize,
1935
+ cleanQueryString,
1936
+ stringToEncodings,
1937
+ unicodeToEncodings,
1386
1938
  };
1387
1939
 
1388
1940
  module.exports = utils;