json-utils.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /**
  2. * JSON 核心纯函数模块 —— 可被 Vitest 直接测试,也供 format-lib / json-worker 引用
  3. */
  4. // ─── HTML 转义 ──────────────────────────────────────────────
  5. export function htmlspecialchars(str) {
  6. return str
  7. .replace(/&/g, '&')
  8. .replace(/</g, '&lt;')
  9. .replace(/>/g, '&gt;')
  10. .replace(/"/g, '&quot;')
  11. .replace(/'/g, '&#039;');
  12. }
  13. // ─── URL 检测 ───────────────────────────────────────────────
  14. export function isUrl(str) {
  15. if (typeof str !== 'string') return false;
  16. return /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i.test(str);
  17. }
  18. // ─── BigNumber duck-typing ──────────────────────────────────
  19. export function isBigNumberLike(value) {
  20. return (
  21. value !== null &&
  22. value !== undefined &&
  23. typeof value === 'object' &&
  24. typeof value.s === 'number' &&
  25. typeof value.e === 'number' &&
  26. Array.isArray(value.c)
  27. );
  28. }
  29. // ─── 类型判断 ───────────────────────────────────────────────
  30. export function getType(value) {
  31. if (value === null) return 'null';
  32. if (value === undefined) return 'undefined';
  33. if (typeof value === 'bigint') return 'bigint';
  34. if (typeof value === 'object') {
  35. if (isBigNumberLike(value)) return 'bigint';
  36. if (Array.isArray(value)) return 'array';
  37. return 'object';
  38. }
  39. return typeof value;
  40. }
  41. // ─── BigNumber → 字符串 ─────────────────────────────────────
  42. export function rebuildBigNumberFromParts(value) {
  43. const sign = value.s < 0 ? '-' : '';
  44. const CHUNK_SIZE = 14;
  45. let digits = '';
  46. for (let i = 0; i < value.c.length; i++) {
  47. let chunkStr = Math.abs(value.c[i]).toString();
  48. if (i > 0) {
  49. chunkStr = chunkStr.padStart(CHUNK_SIZE, '0');
  50. }
  51. digits += chunkStr;
  52. }
  53. digits = digits.replace(/^0+/, '') || '0';
  54. const decimalIndex = value.e + 1;
  55. if (decimalIndex <= 0) {
  56. const zeros = '0'.repeat(Math.abs(decimalIndex));
  57. let fraction = zeros + digits;
  58. fraction = fraction.replace(/0+$/, '');
  59. if (!fraction) return sign + '0';
  60. return sign + '0.' + fraction;
  61. }
  62. if (decimalIndex >= digits.length) {
  63. return sign + digits + '0'.repeat(decimalIndex - digits.length);
  64. }
  65. const intPart = digits.slice(0, decimalIndex);
  66. let fracPart = digits.slice(decimalIndex).replace(/0+$/, '');
  67. if (!fracPart) return sign + intPart;
  68. return sign + intPart + '.' + fracPart;
  69. }
  70. export function getBigNumberDisplayString(value) {
  71. if (typeof value === 'bigint') return value.toString();
  72. if (!isBigNumberLike(value)) return String(value);
  73. if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) {
  74. try {
  75. const result = value.toString();
  76. if (typeof result === 'string' && result !== '[object Object]') {
  77. return result;
  78. }
  79. } catch (_) {}
  80. }
  81. return rebuildBigNumberFromParts(value);
  82. }
  83. // ─── BigInt 安全解析(统一实现,替代 format-lib 和 json-worker 各自的版本)──
  84. export function parseWithBigInt(text) {
  85. text = String(text).trim();
  86. // 1) 宽松修正:单引号 key → 双引号 key
  87. let fixed = text.replace(/([\{,]\s*)'([^'\\]*?)'(\s*:)/g, '$1"$2"$3');
  88. // 2) 未加引号 key 补双引号
  89. fixed = fixed.replace(/([\{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
  90. // 3) 单引号值 → 双引号值(仅对 : 后面的单引号字符串)
  91. fixed = fixed.replace(/(:\s*)'([^'\\]*?)'/g, '$1"$2"');
  92. // 3) 标记 16 位及以上的整数,确保不在字符串内部
  93. const marked = fixed.replace(
  94. /([:,\[]\s*)(-?\d{16,})(\s*)(?=[,\]\}])/g,
  95. function (match, prefix, number, spaces, offset) {
  96. let inStr = false;
  97. let esc = false;
  98. for (let i = 0; i < offset; i++) {
  99. if (esc) { esc = false; continue; }
  100. if (fixed[i] === '\\') { esc = true; continue; }
  101. if (fixed[i] === '"') inStr = !inStr;
  102. }
  103. if (inStr) return match;
  104. return prefix + '"__BigInt__' + number + '"' + spaces;
  105. },
  106. );
  107. return JSON.parse(marked, function (_key, value) {
  108. if (typeof value === 'string' && value.startsWith('__BigInt__')) {
  109. try {
  110. return BigInt(value.slice(10));
  111. } catch (_) {
  112. return value.slice(10);
  113. }
  114. }
  115. return value;
  116. });
  117. }
  118. // ─── 深度解包嵌套 JSON 字符串 ──────────────────────────────
  119. export function deepParseJSONStrings(obj) {
  120. if (Array.isArray(obj)) {
  121. return obj.map((item) => {
  122. if (typeof item === 'string' && item.trim()) {
  123. try {
  124. const parsed = JSON.parse(item);
  125. if (_isDeepParsable(parsed)) {
  126. return deepParseJSONStrings(parsed);
  127. }
  128. } catch (_) {}
  129. }
  130. return deepParseJSONStrings(item);
  131. });
  132. }
  133. if (typeof obj === 'object' && obj !== null) {
  134. const newObj = {};
  135. for (const key of Object.keys(obj)) {
  136. const val = obj[key];
  137. if (typeof val === 'string' && val.trim()) {
  138. try {
  139. const parsed = JSON.parse(val);
  140. if (_isDeepParsable(parsed)) {
  141. newObj[key] = deepParseJSONStrings(parsed);
  142. continue;
  143. }
  144. } catch (_) {}
  145. }
  146. newObj[key] = deepParseJSONStrings(val);
  147. }
  148. return newObj;
  149. }
  150. return obj;
  151. }
  152. function _isDeepParsable(parsed) {
  153. if (typeof parsed !== 'object' || parsed === null) return false;
  154. if (!Array.isArray(parsed) && Object.prototype.toString.call(parsed) !== '[object Object]') return false;
  155. // 排除 BigNumber duck-type {s, e, c}
  156. if (isBigNumberLike(parsed) && Object.keys(parsed).length === 3) return false;
  157. return true;
  158. }
  159. // ─── Unicode 编解码 ─────────────────────────────────────────
  160. export function uniEncode(str) {
  161. return escape(str)
  162. .replace(/%u/gi, '\\u')
  163. .replace(/%7b/gi, '{')
  164. .replace(/%7d/gi, '}')
  165. .replace(/%3a/gi, ':')
  166. .replace(/%2c/gi, ',')
  167. .replace(/%27/gi, "'")
  168. .replace(/%22/gi, '"')
  169. .replace(/%5b/gi, '[')
  170. .replace(/%5d/gi, ']')
  171. .replace(/%3D/gi, '=')
  172. .replace(/%08/gi, '\b')
  173. .replace(/%0D/gi, '\r')
  174. .replace(/%0C/gi, '\f')
  175. .replace(/%09/gi, '\t')
  176. .replace(/%20/gi, ' ')
  177. .replace(/%0A/gi, '\n')
  178. .replace(/%3E/gi, '>')
  179. .replace(/%3C/gi, '<')
  180. .replace(/%3F/gi, '?');
  181. }
  182. export function uniDecode(text) {
  183. text = text.replace(/(\\)?\\u/gi, '%u').replace('%u0025', '%25');
  184. text = unescape(text.toString().replace(/%2B/g, '+'));
  185. const matches = text.match(/(%u00([0-9A-F]{2}))/gi);
  186. if (matches) {
  187. for (const m of matches) {
  188. const code = m.substring(1, 3);
  189. const x = Number('0x' + code);
  190. if (x >= 128) text = text.replace(m, code);
  191. }
  192. }
  193. return unescape(text.toString().replace(/%2B/g, '+'));
  194. }
  195. // ─── 安全的 safeStringify(保留 BigInt 精度)──────────────
  196. export function safeStringify(obj, space) {
  197. const tagged = JSON.stringify(
  198. obj,
  199. function (_key, value) {
  200. if (typeof value === 'bigint') {
  201. return `__FH_BIGINT__${value.toString()}`;
  202. }
  203. if (typeof value === 'number' && value.toString().includes('e')) {
  204. return `__FH_NUMSTR__${value.toLocaleString('fullwide', { useGrouping: false })}`;
  205. }
  206. return value;
  207. },
  208. space,
  209. );
  210. return tagged
  211. .replace(/"__FH_BIGINT__(-?\d+)"/g, '$1')
  212. .replace(/"__FH_NUMSTR__(-?\d+)"/g, '$1');
  213. }
  214. // ─── 日期格式化(替代 Date.prototype.format 的纯函数版本)──
  215. export function formatDate(date, pattern) {
  216. const pad = (src, len) => {
  217. const neg = src < 0;
  218. let s = String(Math.abs(src));
  219. while (s.length < len) s = '0' + s;
  220. return (neg ? '-' : '') + s;
  221. };
  222. if (typeof pattern !== 'string') return date.toString();
  223. const y = date.getFullYear();
  224. const M = date.getMonth() + 1;
  225. const d = date.getDate();
  226. const H = date.getHours();
  227. const m = date.getMinutes();
  228. const s = date.getSeconds();
  229. const S = date.getMilliseconds();
  230. return pattern
  231. .replace(/yyyy/g, pad(y, 4))
  232. .replace(/yy/g, pad(parseInt(y.toString().slice(2), 10), 2))
  233. .replace(/MM/g, pad(M, 2))
  234. .replace(/M/g, M)
  235. .replace(/dd/g, pad(d, 2))
  236. .replace(/d/g, d)
  237. .replace(/HH/g, pad(H, 2))
  238. .replace(/H/g, H)
  239. .replace(/hh/g, pad(H % 12, 2))
  240. .replace(/h/g, H % 12)
  241. .replace(/mm/g, pad(m, 2))
  242. .replace(/ss/g, pad(s, 2))
  243. .replace(/SSS/g, pad(S, 3))
  244. .replace(/S/g, S);
  245. }
  246. // ─── 字符串字节数(替代 String.prototype.getBytes 的纯函数版本)──
  247. export function getStringBytes(str) {
  248. const stream = str.replace(/\n/g, 'xx').replace(/\t/g, 'x');
  249. const escaped = encodeURIComponent(stream);
  250. return escaped.replace(/%[A-Z0-9][A-Z0-9]/g, 'x').length;
  251. }
  252. // ─── toast 安全版(XSS 防护)────────────────────────────────
  253. export function createSafeToastHTML(content) {
  254. const safe = htmlspecialchars(content);
  255. return (
  256. '<div id="fehelper_alertmsg" style="position:fixed;top:5px;right:5px;z-index:1000000">' +
  257. '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
  258. 'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' +
  259. safe +
  260. '</p></div>'
  261. );
  262. }