format-lib.js 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861
  1. /**
  2. * 日期格式化
  3. * @param {Object} pattern
  4. */
  5. Date.prototype.format = function (pattern) {
  6. let pad = function (source, length) {
  7. let pre = "",
  8. negative = (source < 0),
  9. string = String(Math.abs(source));
  10. if (string.length < length) {
  11. pre = (new Array(length - string.length + 1)).join('0');
  12. }
  13. return (negative ? "-" : "") + pre + string;
  14. };
  15. if ('string' !== typeof pattern) {
  16. return this.toString();
  17. }
  18. let replacer = function (patternPart, result) {
  19. pattern = pattern.replace(patternPart, result);
  20. };
  21. let year = this.getFullYear(),
  22. month = this.getMonth() + 1,
  23. date2 = this.getDate(),
  24. hours = this.getHours(),
  25. minutes = this.getMinutes(),
  26. seconds = this.getSeconds(),
  27. milliSec = this.getMilliseconds();
  28. replacer(/yyyy/g, pad(year, 4));
  29. replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
  30. replacer(/MM/g, pad(month, 2));
  31. replacer(/M/g, month);
  32. replacer(/dd/g, pad(date2, 2));
  33. replacer(/d/g, date2);
  34. replacer(/HH/g, pad(hours, 2));
  35. replacer(/H/g, hours);
  36. replacer(/hh/g, pad(hours % 12, 2));
  37. replacer(/h/g, hours % 12);
  38. replacer(/mm/g, pad(minutes, 2));
  39. replacer(/m/g, minutes);
  40. replacer(/ss/g, pad(seconds, 2));
  41. replacer(/s/g, seconds);
  42. replacer(/SSS/g, pad(milliSec, 3));
  43. replacer(/S/g, milliSec);
  44. return pattern;
  45. };
  46. /**
  47. * 自动消失的Alert弹窗
  48. * @param content
  49. */
  50. window.toast = function (content) {
  51. window.clearTimeout(window.feHelperAlertMsgTid);
  52. let elAlertMsg = document.querySelector("#fehelper_alertmsg");
  53. if (!elAlertMsg) {
  54. let elWrapper = document.createElement('div');
  55. elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:25px;left:5px;z-index:1000000">' +
  56. '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
  57. 'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
  58. elAlertMsg = elWrapper.childNodes[0];
  59. document.body.appendChild(elAlertMsg);
  60. } else {
  61. elAlertMsg.querySelector('p').innerHTML = content;
  62. elAlertMsg.style.display = 'block';
  63. }
  64. window.feHelperAlertMsgTid = window.setTimeout(function () {
  65. elAlertMsg.style.display = 'none';
  66. }, 1000);
  67. };
  68. /**
  69. * FeHelper Json Format Lib,入口文件
  70. * @example
  71. * Formatter.format(jsonString)
  72. */
  73. window.Formatter = (function () {
  74. "use strict";
  75. let jfContent,
  76. jfPre,
  77. jfStyleEl,
  78. jfStatusBar,
  79. formattingMsg;
  80. let lastItemIdGiven = 0;
  81. let cachedJsonString = '';
  82. // 单例Worker实例
  83. let workerInstance = null;
  84. // CSP限制标记,避免重复尝试创建Worker
  85. let cspRestricted = false;
  86. // 转义功能开启标记
  87. let escapeJsonStringEnabled = false;
  88. let _initElements = function () {
  89. jfContent = $('#jfContent');
  90. if (!jfContent[0]) {
  91. jfContent = $('<div id="jfContent" />').appendTo('body');
  92. }
  93. jfPre = $('#jfContent_pre');
  94. if (!jfPre[0]) {
  95. jfPre = $('<pre id="jfContent_pre" />').appendTo('body');
  96. }
  97. jfStyleEl = $('#jfStyleEl');
  98. if (!jfStyleEl[0]) {
  99. jfStyleEl = $('<style id="jfStyleEl" />').appendTo('head');
  100. }
  101. formattingMsg = $('#formattingMsg');
  102. if (!formattingMsg[0]) {
  103. formattingMsg = $('<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>').appendTo('body');
  104. }
  105. try {
  106. jfContent.html('').show();
  107. jfPre.html('').hide();
  108. jfStatusBar && jfStatusBar.hide();
  109. formattingMsg.hide();
  110. } catch (e) {
  111. }
  112. };
  113. /**
  114. * HTML特殊字符格式化
  115. * @param str
  116. * @returns {*}
  117. */
  118. let htmlspecialchars = function (str) {
  119. str = str.replace(/&/g, '&amp;');
  120. str = str.replace(/</g, '&lt;');
  121. str = str.replace(/>/g, '&gt;');
  122. str = str.replace(/"/g, '&quot;');
  123. str = str.replace(/'/g, '&#039;');
  124. str = str.replace(/\\/g, '&#92;');
  125. return str;
  126. };
  127. /**
  128. * 直接下载,能解决中文乱码
  129. * @param content
  130. * @private
  131. */
  132. let _downloadSupport = function (content) {
  133. // 下载链接
  134. let dt = (new Date()).format('yyyyMMddHHmmss');
  135. let blob = new Blob([content], {type: 'application/octet-stream'});
  136. let button = $('<button class="xjf-btn xjf-btn-right">下载JSON</button>').appendTo('#optionBar');
  137. // 检查是否在沙盒化iframe中
  138. function isSandboxed() {
  139. try {
  140. return window !== window.top || window.parent !== window;
  141. } catch (e) {
  142. return true;
  143. }
  144. }
  145. // 在沙盒模式下显示JSON内容
  146. function showJsonContentInSandbox() {
  147. // 查找 #formattedJson 节点
  148. let formattedJsonDiv = document.getElementById('formattedJson');
  149. if (!formattedJsonDiv) {
  150. console.error('未找到 #formattedJson 节点');
  151. return;
  152. }
  153. // 清空 #formattedJson 的内容
  154. formattedJsonDiv.innerHTML = '';
  155. // 创建下载提示和内容显示区域
  156. let downloadInfo = document.createElement('div');
  157. downloadInfo.style.cssText = `
  158. background: #e3f2fd;
  159. border: 1px solid #2196f3;
  160. border-radius: 4px;
  161. padding: 15px;
  162. margin-bottom: 15px;
  163. font-family: Arial, sans-serif;
  164. `;
  165. downloadInfo.innerHTML = `
  166. <div style="color: #1976d2; font-weight: bold; margin-bottom: 8px;">📋 沙盒模式 - JSON内容</div>
  167. <div style="color: #666; font-size: 14px; margin-bottom: 10px;">由于浏览器安全限制,无法直接下载。请复制以下内容并保存为 .json 文件:</div>
  168. <button onclick="
  169. let textarea = this.parentElement.nextElementSibling;
  170. textarea.select();
  171. document.execCommand('copy');
  172. alert('已复制到剪贴板!');
  173. " style="
  174. background: #2196f3;
  175. color: white;
  176. border: none;
  177. padding: 8px 16px;
  178. border-radius: 4px;
  179. cursor: pointer;
  180. font-size: 14px;
  181. ">复制全部内容</button>
  182. `;
  183. // 创建文本区域
  184. let textarea = document.createElement('textarea');
  185. textarea.style.cssText = `
  186. width: 100%;
  187. height: 300px;
  188. font-family: 'Courier New', monospace;
  189. font-size: 12px;
  190. border: 1px solid #ddd;
  191. padding: 15px;
  192. border-radius: 4px;
  193. resize: vertical;
  194. box-sizing: border-box;
  195. background: #f8f9fa;
  196. `;
  197. textarea.value = content;
  198. textarea.readOnly = true;
  199. // 将内容添加到 #formattedJson 节点
  200. formattedJsonDiv.appendChild(downloadInfo);
  201. formattedJsonDiv.appendChild(textarea);
  202. console.log('JSON内容已显示在 #formattedJson 节点中');
  203. }
  204. // 显示JSON内容模态框(非沙盒模式)
  205. function showJsonContent() {
  206. let modal = document.createElement('div');
  207. modal.style.cssText = `
  208. position: fixed;
  209. top: 0;
  210. left: 0;
  211. width: 100%;
  212. height: 100%;
  213. background: rgba(0,0,0,0.8);
  214. z-index: 999999;
  215. display: flex;
  216. align-items: center;
  217. justify-content: center;
  218. font-family: Arial, sans-serif;
  219. `;
  220. modal.innerHTML = `
  221. <div style="
  222. background: white;
  223. padding: 20px;
  224. border-radius: 8px;
  225. max-width: 90%;
  226. max-height: 90%;
  227. box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  228. position: relative;
  229. ">
  230. <h3 style="margin: 0 0 15px 0; color: #333; font-size: 18px;">JSON内容</h3>
  231. <p style="color: #666; font-size: 14px; margin: 0 0 15px 0;">请复制以下内容并保存为 .json 文件:</p>
  232. <textarea readonly style="
  233. width: 100%;
  234. height: 400px;
  235. font-family: 'Courier New', monospace;
  236. font-size: 12px;
  237. border: 1px solid #ddd;
  238. padding: 15px;
  239. border-radius: 4px;
  240. resize: vertical;
  241. box-sizing: border-box;
  242. ">${content}</textarea>
  243. <div style="margin-top: 15px; text-align: right;">
  244. <button onclick="this.closest('div').parentElement.remove()" style="
  245. background: #6c757d;
  246. color: white;
  247. border: none;
  248. padding: 10px 20px;
  249. border-radius: 4px;
  250. cursor: pointer;
  251. margin-right: 10px;
  252. font-size: 14px;
  253. ">关闭</button>
  254. <button onclick="
  255. this.previousElementSibling.previousElementSibling.select();
  256. document.execCommand('copy');
  257. alert('已复制到剪贴板!');
  258. " style="
  259. background: #007bff;
  260. color: white;
  261. border: none;
  262. padding: 10px 20px;
  263. border-radius: 4px;
  264. cursor: pointer;
  265. font-size: 14px;
  266. ">复制全部</button>
  267. </div>
  268. </div>
  269. `;
  270. document.body.appendChild(modal);
  271. // 点击背景关闭
  272. modal.addEventListener('click', function(e) {
  273. if (e.target === modal) {
  274. document.body.removeChild(modal);
  275. }
  276. });
  277. }
  278. // 尝试下载
  279. function tryDownload() {
  280. try {
  281. let aLink = document.createElement('a');
  282. aLink.download = 'FeHelper-' + dt + '.json';
  283. aLink.href = URL.createObjectURL(blob);
  284. aLink.style.display = 'none';
  285. document.body.appendChild(aLink);
  286. aLink.click();
  287. setTimeout(() => {
  288. if (document.body.contains(aLink)) {
  289. document.body.removeChild(aLink);
  290. }
  291. URL.revokeObjectURL(aLink.href);
  292. }, 100);
  293. return true;
  294. } catch (error) {
  295. console.error('下载失败:', error);
  296. return false;
  297. }
  298. }
  299. // 下载按钮点击事件
  300. button.click(function (e) {
  301. e.preventDefault();
  302. // 如果在沙盒化环境中,在 #formattedJson 中显示内容
  303. if (isSandboxed()) {
  304. console.log('检测到沙盒化环境,在 #formattedJson 中显示内容');
  305. showJsonContentInSandbox();
  306. return;
  307. }
  308. // 尝试Chrome扩展API
  309. if (typeof chrome !== 'undefined' && chrome.downloads) {
  310. try {
  311. chrome.downloads.download({
  312. url: URL.createObjectURL(blob),
  313. saveAs: true,
  314. conflictAction: 'overwrite',
  315. filename: 'FeHelper-' + dt + '.json'
  316. }, (downloadId) => {
  317. if (chrome.runtime.lastError) {
  318. console.error('Chrome下载失败:', chrome.runtime.lastError);
  319. showJsonContent();
  320. } else {
  321. console.log('Chrome下载成功,ID:', downloadId);
  322. }
  323. });
  324. } catch (error) {
  325. console.error('Chrome下载API调用失败:', error);
  326. showJsonContent();
  327. }
  328. } else {
  329. // 尝试标准下载
  330. if (!tryDownload()) {
  331. showJsonContent();
  332. }
  333. }
  334. });
  335. };
  336. /**
  337. * chrome 下复制到剪贴板
  338. * @param text
  339. */
  340. let _copyToClipboard = function (text) {
  341. let input = document.createElement('textarea');
  342. input.style.position = 'fixed';
  343. input.style.opacity = 0;
  344. input.value = text;
  345. document.body.appendChild(input);
  346. input.select();
  347. document.execCommand('Copy');
  348. document.body.removeChild(input);
  349. toast('Json片段复制成功,随处粘贴可用!')
  350. };
  351. /**
  352. * 从el中获取json文本
  353. * @param el
  354. * @returns {string}
  355. */
  356. let getJsonText = function (el) {
  357. let txt = el.text().replace(/复制\|下载\|删除/gm,'').replace(/":\s/gm, '":').replace(/,$/, '').trim();
  358. if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
  359. txt = '{' + txt + '}';
  360. }
  361. try {
  362. txt = JSON.stringify(JSON.parse(txt), null, 4);
  363. } catch (err) {
  364. }
  365. return txt;
  366. };
  367. // 添加json路径
  368. let _showJsonPath = function (curEl) {
  369. let keys = [];
  370. let current = curEl;
  371. // 处理当前节点
  372. if (current.hasClass('item') && !current.hasClass('rootItem')) {
  373. if (current.hasClass('item-array-element')) {
  374. // 这是数组元素,使用data-array-index属性
  375. let index = current.attr('data-array-index');
  376. if (index !== undefined) {
  377. keys.unshift('[' + index + ']');
  378. }
  379. } else {
  380. // 这是对象属性,获取key
  381. let keyText = current.find('>.key').text();
  382. if (keyText) {
  383. keys.unshift(keyText);
  384. }
  385. }
  386. }
  387. // 向上遍历所有祖先节点
  388. current.parents('.item').each(function() {
  389. let $this = $(this);
  390. // 跳过根节点
  391. if ($this.hasClass('rootItem')) {
  392. return false; // 终止遍历
  393. }
  394. if ($this.hasClass('item-array-element')) {
  395. // 这是数组元素,使用data-array-index属性
  396. let index = $this.attr('data-array-index');
  397. if (index !== undefined) {
  398. keys.unshift('[' + index + ']');
  399. }
  400. } else if ($this.hasClass('item-object') || $this.hasClass('item-array')) {
  401. // 这是容器节点,寻找它的key
  402. let $container = $this.parent().parent(); // 跳过 .kv-list
  403. if ($container.length && !$container.hasClass('rootItem')) {
  404. if ($container.hasClass('item-array-element')) {
  405. // 容器本身是数组元素
  406. let index = $container.attr('data-array-index');
  407. if (index !== undefined) {
  408. keys.unshift('[' + index + ']');
  409. }
  410. } else {
  411. // 容器是对象属性
  412. let keyText = $container.find('>.key').text();
  413. if (keyText) {
  414. keys.unshift(keyText);
  415. }
  416. }
  417. }
  418. } else {
  419. // 普通item节点,获取key
  420. let keyText = $this.find('>.key').text();
  421. if (keyText) {
  422. keys.unshift(keyText);
  423. }
  424. }
  425. });
  426. // 过滤掉空值和无效的key
  427. let validKeys = keys.filter(key => key && key.trim() !== '');
  428. // 创建或获取语言选择器和路径显示区域
  429. let jfPathContainer = $('#jsonPathContainer');
  430. if (!jfPathContainer.length) {
  431. jfPathContainer = $('<div id="jsonPathContainer"/>').prependTo(jfStatusBar);
  432. // 创建语言选择下拉框
  433. let langSelector = $('<select id="jsonPathLangSelector" title="选择编程语言格式">' +
  434. '<option value="javascript">JavaScript</option>' +
  435. '<option value="php">PHP</option>' +
  436. '<option value="python">Python</option>' +
  437. '<option value="java">Java</option>' +
  438. '<option value="csharp">C#</option>' +
  439. '<option value="golang">Go</option>' +
  440. '<option value="ruby">Ruby</option>' +
  441. '<option value="swift">Swift</option>' +
  442. '</select>').appendTo(jfPathContainer);
  443. // 创建路径显示区域
  444. let jfPath = $('<span id="jsonPath"/>').appendTo(jfPathContainer);
  445. // 绑定语言切换事件
  446. langSelector.on('change', function() {
  447. // 保存选择的语言到本地存储(如果可用)
  448. try {
  449. localStorage.setItem('fehelper_json_path_lang', $(this).val());
  450. } catch (e) {
  451. // 在沙盒环境中忽略localStorage错误
  452. console.warn('localStorage不可用,跳过保存语言选择');
  453. }
  454. // 从容器中获取当前保存的keys,而不是使用闭包中的validKeys
  455. let currentKeys = jfPathContainer.data('currentKeys') || [];
  456. _updateJsonPath(currentKeys, $(this).val());
  457. });
  458. // 从本地存储恢复语言选择(如果可用)
  459. let savedLang = 'javascript';
  460. try {
  461. savedLang = localStorage.getItem('fehelper_json_path_lang') || 'javascript';
  462. } catch (e) {
  463. // 在沙盒环境中使用默认值
  464. console.warn('localStorage不可用,使用默认语言选择');
  465. }
  466. langSelector.val(savedLang);
  467. }
  468. // 保存当前的keys到容器的data属性中,供语言切换时使用
  469. jfPathContainer.data('currentKeys', validKeys);
  470. // 获取当前选择的语言
  471. let selectedLang = $('#jsonPathLangSelector').val() || 'javascript';
  472. _updateJsonPath(validKeys, selectedLang);
  473. };
  474. // 根据不同编程语言格式化JSON路径
  475. let _updateJsonPath = function(keys, language) {
  476. let path = _formatJsonPath(keys, language);
  477. $('#jsonPath').html('当前节点:' + path);
  478. };
  479. // 格式化JSON路径为不同编程语言格式
  480. let _formatJsonPath = function(keys, language) {
  481. if (!keys.length) {
  482. return _getLanguageRoot(language);
  483. }
  484. let path = '';
  485. switch (language) {
  486. case 'javascript':
  487. path = '$';
  488. for (let i = 0; i < keys.length; i++) {
  489. let key = keys[i];
  490. if (key.startsWith('[') && key.endsWith(']')) {
  491. // 数组索引
  492. path += key;
  493. } else {
  494. // 对象属性
  495. if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
  496. // 有效的标识符,使用点语法
  497. path += '.' + key;
  498. } else {
  499. // 包含特殊字符,使用方括号语法
  500. path += '["' + key.replace(/"/g, '\\"') + '"]';
  501. }
  502. }
  503. }
  504. break;
  505. case 'php':
  506. path = '$data';
  507. for (let i = 0; i < keys.length; i++) {
  508. let key = keys[i];
  509. if (key.startsWith('[') && key.endsWith(']')) {
  510. // 数组索引
  511. path += key;
  512. } else {
  513. // 对象属性
  514. path += '["' + key.replace(/"/g, '\\"') + '"]';
  515. }
  516. }
  517. break;
  518. case 'python':
  519. path = 'data';
  520. for (let i = 0; i < keys.length; i++) {
  521. let key = keys[i];
  522. if (key.startsWith('[') && key.endsWith(']')) {
  523. // 数组索引
  524. path += key;
  525. } else {
  526. // 对象属性
  527. if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && !/^(and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield)$/.test(key)) {
  528. // 有效的标识符且不是关键字,可以使用点语法
  529. path += '.' + key;
  530. } else {
  531. // 使用方括号语法
  532. path += '["' + key.replace(/"/g, '\\"') + '"]';
  533. }
  534. }
  535. }
  536. break;
  537. case 'java':
  538. path = 'jsonObject';
  539. for (let i = 0; i < keys.length; i++) {
  540. let key = keys[i];
  541. if (key.startsWith('[') && key.endsWith(']')) {
  542. // 数组索引
  543. let index = key.slice(1, -1);
  544. path += '.get(' + index + ')';
  545. } else {
  546. // 对象属性
  547. path += '.get("' + key.replace(/"/g, '\\"') + '")';
  548. }
  549. }
  550. break;
  551. case 'csharp':
  552. path = 'jsonObject';
  553. for (let i = 0; i < keys.length; i++) {
  554. let key = keys[i];
  555. if (key.startsWith('[') && key.endsWith(']')) {
  556. // 数组索引
  557. path += key;
  558. } else {
  559. // 对象属性
  560. path += '["' + key.replace(/"/g, '\\"') + '"]';
  561. }
  562. }
  563. break;
  564. case 'golang':
  565. path = 'data';
  566. for (let i = 0; i < keys.length; i++) {
  567. let key = keys[i];
  568. if (key.startsWith('[') && key.endsWith(']')) {
  569. // 数组索引
  570. let index = key.slice(1, -1);
  571. path += '.(' + index + ')';
  572. } else {
  573. // 对象属性
  574. path += '["' + key.replace(/"/g, '\\"') + '"]';
  575. }
  576. }
  577. break;
  578. case 'ruby':
  579. path = 'data';
  580. for (let i = 0; i < keys.length; i++) {
  581. let key = keys[i];
  582. if (key.startsWith('[') && key.endsWith(']')) {
  583. // 数组索引
  584. path += key;
  585. } else {
  586. // 对象属性
  587. if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
  588. // 可以使用符号访问
  589. path += '[:"' + key + '"]';
  590. } else {
  591. // 字符串键
  592. path += '["' + key.replace(/"/g, '\\"') + '"]';
  593. }
  594. }
  595. }
  596. break;
  597. case 'swift':
  598. path = 'jsonObject';
  599. for (let i = 0; i < keys.length; i++) {
  600. let key = keys[i];
  601. if (key.startsWith('[') && key.endsWith(']')) {
  602. // 数组索引
  603. path += key;
  604. } else {
  605. // 对象属性
  606. path += '["' + key.replace(/"/g, '\\"') + '"]';
  607. }
  608. }
  609. break;
  610. default:
  611. // 默认使用JavaScript格式
  612. return _formatJsonPath(keys, 'javascript');
  613. }
  614. return path;
  615. };
  616. // 获取不同语言的根对象表示
  617. let _getLanguageRoot = function(language) {
  618. switch (language) {
  619. case 'javascript': return '$';
  620. case 'php': return '$data';
  621. case 'python': return 'data';
  622. case 'java': return 'jsonObject';
  623. case 'csharp': return 'jsonObject';
  624. case 'golang': return 'data';
  625. case 'ruby': return 'data';
  626. case 'swift': return 'jsonObject';
  627. default: return '$';
  628. }
  629. };
  630. // 给某个节点增加操作项
  631. let _addOptForItem = function (el, show) {
  632. // 下载json片段
  633. let fnDownload = function (event) {
  634. event.stopPropagation();
  635. let txt = getJsonText(el);
  636. // 下载片段
  637. let dt = (new Date()).format('yyyyMMddHHmmss');
  638. let blob = new Blob([txt], {type: 'application/octet-stream'});
  639. if (typeof chrome === 'undefined' || !chrome.permissions) {
  640. // 下载JSON的简单形式
  641. $(this).attr('download', 'FeHelper-' + dt + '.json').attr('href', URL.createObjectURL(blob));
  642. } else {
  643. // 请求权限
  644. chrome.permissions.request({
  645. permissions: ['downloads']
  646. }, (granted) => {
  647. if (granted) {
  648. chrome.downloads.download({
  649. url: URL.createObjectURL(blob),
  650. saveAs: true,
  651. conflictAction: 'overwrite',
  652. filename: 'FeHelper-' + dt + '.json'
  653. });
  654. } else {
  655. toast('必须接受授权,才能正常下载!');
  656. }
  657. });
  658. }
  659. };
  660. // 复制json片段
  661. let fnCopy = function (event) {
  662. event.stopPropagation();
  663. _copyToClipboard(getJsonText(el));
  664. };
  665. // 删除json片段
  666. let fnDel = function (event) {
  667. event.stopPropagation();
  668. if (el.parent().is('#formattedJson')) {
  669. toast('如果连最外层的Json也删掉的话,就没啥意义了哦!');
  670. return false;
  671. }
  672. toast('节点已删除成功!');
  673. el.remove();
  674. jfStatusBar && jfStatusBar.hide();
  675. };
  676. $('.boxOpt').hide();
  677. if (show) {
  678. let jfOptEl = el.children('.boxOpt');
  679. if (!jfOptEl.length) {
  680. jfOptEl = $('<b class="boxOpt">' +
  681. '<a class="opt-copy" title="复制当前选中节点的JSON数据">复制</a>|' +
  682. '<a class="opt-download" target="_blank" title="下载当前选中节点的JSON数据">下载</a>|' +
  683. '<a class="opt-del" title="删除当前选中节点的JSON数据">删除</a></b>').appendTo(el);
  684. } else {
  685. jfOptEl.show();
  686. }
  687. jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
  688. jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
  689. jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
  690. }
  691. };
  692. // 显示当前节点的Key
  693. let _toogleStatusBar = function (curEl, show) {
  694. if (!jfStatusBar) {
  695. jfStatusBar = $('<div id="statusBar"/>').appendTo('body');
  696. }
  697. if (!show) {
  698. jfStatusBar.hide();
  699. return;
  700. } else {
  701. jfStatusBar.show();
  702. }
  703. _showJsonPath(curEl);
  704. };
  705. /**
  706. * 递归折叠所有层级的对象和数组节点
  707. * @param elements
  708. */
  709. function collapse(elements) {
  710. elements.each(function () {
  711. var el = $(this);
  712. if (el.children('.kv-list').length) {
  713. el.addClass('collapsed');
  714. // 只给没有id的节点分配唯一id,并生成注释
  715. if (!el.attr('id')) {
  716. el.attr('id', 'item' + (++lastItemIdGiven));
  717. let count = el.children('.kv-list').eq(0).children().length;
  718. let comment = count + (count === 1 ? ' item' : ' items');
  719. jfStyleEl[0].insertAdjacentHTML(
  720. 'beforeend',
  721. '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
  722. );
  723. }
  724. // 递归对子节点继续折叠,确保所有嵌套层级都被处理
  725. collapse(el.children('.kv-list').children('.item-object, .item-block'));
  726. }
  727. });
  728. }
  729. /**
  730. * 创建几个全局操作的按钮,置于页面右上角即可
  731. * @private
  732. */
  733. let _buildOptionBar = function () {
  734. let optionBar = $('#optionBar');
  735. if (optionBar.length) {
  736. optionBar.html('');
  737. } else {
  738. optionBar = $('<span id="optionBar" />').appendTo(jfContent.parent());
  739. }
  740. $('<span class="x-split">|</span>').appendTo(optionBar);
  741. let buttonFormatted = $('<button class="xjf-btn xjf-btn-left">元数据</button>').appendTo(optionBar);
  742. let buttonCollapseAll = $('<button class="xjf-btn xjf-btn-mid">折叠所有</button>').appendTo(optionBar);
  743. let plainOn = false;
  744. buttonFormatted.bind('click', function (e) {
  745. if (plainOn) {
  746. plainOn = false;
  747. jfPre.hide();
  748. jfContent.show();
  749. buttonFormatted.text('元数据');
  750. } else {
  751. plainOn = true;
  752. jfPre.show();
  753. jfContent.hide();
  754. buttonFormatted.text('格式化');
  755. }
  756. jfStatusBar && jfStatusBar.hide();
  757. });
  758. buttonCollapseAll.bind('click', function (e) {
  759. // 如果内容还没有格式化过,需要再格式化一下
  760. if (plainOn) {
  761. buttonFormatted.trigger('click');
  762. }
  763. if (buttonCollapseAll.text() === '折叠所有') {
  764. buttonCollapseAll.text('展开所有');
  765. // 递归折叠所有层级的对象和数组,确保所有内容都被折叠
  766. collapse($('#jfContent .item-object, #jfContent .item-block'));
  767. } else {
  768. buttonCollapseAll.text('折叠所有');
  769. // 展开所有内容
  770. $('.item-object,.item-block').removeClass('collapsed');
  771. }
  772. jfStatusBar && jfStatusBar.hide();
  773. });
  774. };
  775. // 附加操作
  776. let _addEvents = function () {
  777. // 折叠、展开
  778. $('#jfContent span.expand').bind('click', function (ev) {
  779. ev.preventDefault();
  780. ev.stopPropagation();
  781. let parentEl = $(this).parent();
  782. parentEl.toggleClass('collapsed');
  783. if (parentEl.hasClass('collapsed')) {
  784. collapse(parentEl);
  785. }
  786. });
  787. // 点击选中:高亮
  788. $('#jfContent .item').bind('click', function (e) {
  789. let el = $(this);
  790. if (el.hasClass('x-selected')) {
  791. _toogleStatusBar(el, false);
  792. _addOptForItem(el, false);
  793. el.removeClass('x-selected');
  794. e.stopPropagation();
  795. return true;
  796. }
  797. $('.x-selected').removeClass('x-selected');
  798. el.addClass('x-selected');
  799. // 显示底部状态栏
  800. _toogleStatusBar(el, true);
  801. _addOptForItem(el, true);
  802. if (!$(e.target).is('.item .expand')) {
  803. e.stopPropagation();
  804. } else {
  805. $(e.target).parent().trigger('click');
  806. }
  807. // 触发钩子
  808. if (typeof window._OnJsonItemClickByFH === 'function') {
  809. window._OnJsonItemClickByFH(getJsonText(el));
  810. }
  811. });
  812. // 行悬停效果:只高亮当前直接悬停的item,避免嵌套冒泡
  813. let currentHoverElement = null;
  814. $('#jfContent .item').bind('mouseenter', function (e) {
  815. // 只处理视觉效果,不触发任何其他逻辑
  816. // 清除之前的悬停样式
  817. if (currentHoverElement) {
  818. currentHoverElement.removeClass('fh-hover');
  819. }
  820. // 添加当前悬停样式
  821. let el = $(this);
  822. el.addClass('fh-hover');
  823. currentHoverElement = el;
  824. // 严格阻止事件冒泡和默认行为
  825. e.stopPropagation();
  826. e.stopImmediatePropagation();
  827. e.preventDefault();
  828. });
  829. $('#jfContent .item').bind('mouseleave', function (e) {
  830. // 只处理视觉效果,不触发任何其他逻辑
  831. let el = $(this);
  832. el.removeClass('fh-hover');
  833. // 如果当前移除的元素是记录的悬停元素,清空记录
  834. if (currentHoverElement && currentHoverElement[0] === el[0]) {
  835. currentHoverElement = null;
  836. }
  837. // 严格阻止事件冒泡和默认行为
  838. e.stopPropagation();
  839. e.stopImmediatePropagation();
  840. });
  841. // 为整个jfContent区域添加鼠标离开事件,确保彻底清除悬停样式
  842. $('#jfContent').bind('mouseleave', function (e) {
  843. if (currentHoverElement) {
  844. currentHoverElement.removeClass('fh-hover');
  845. currentHoverElement = null;
  846. }
  847. });
  848. // 图片预览功能:针对所有data-is-link=1的a标签
  849. let $imgPreview = null;
  850. // 加载缓存
  851. function getImgCache() {
  852. try {
  853. return JSON.parse(sessionStorage.getItem('fehelper-img-preview-cache') || '{}');
  854. } catch (e) { return {}; }
  855. }
  856. function setImgCache(url, isImg) {
  857. let cache = getImgCache();
  858. cache[url] = isImg;
  859. sessionStorage.setItem('fehelper-img-preview-cache', JSON.stringify(cache));
  860. }
  861. $('#jfContent').on('mouseenter', 'a[data-is-link="1"]', function(e) {
  862. const url = $(this).attr('data-link-url');
  863. if (!url) return;
  864. let cache = getImgCache();
  865. if (cache.hasOwnProperty(url)) {
  866. if (cache[url]) {
  867. $imgPreview = getOrCreateImgPreview();
  868. $imgPreview.find('img').attr('src', url);
  869. $imgPreview.show();
  870. $(document).on('mousemove.fhimg', function(ev) {
  871. $imgPreview.css({
  872. left: ev.pageX + 20 + 'px',
  873. top: ev.pageY + 20 + 'px'
  874. });
  875. });
  876. $imgPreview.css({
  877. left: e.pageX + 20 + 'px',
  878. top: e.pageY + 20 + 'px'
  879. });
  880. }
  881. return;
  882. }
  883. // 创建图片对象尝试加载
  884. const img = new window.Image();
  885. img.src = url;
  886. img.onload = function() {
  887. setImgCache(url, true);
  888. $imgPreview = getOrCreateImgPreview();
  889. $imgPreview.find('img').attr('src', url);
  890. $imgPreview.show();
  891. $(document).on('mousemove.fhimg', function(ev) {
  892. $imgPreview.css({
  893. left: ev.pageX + 20 + 'px',
  894. top: ev.pageY + 20 + 'px'
  895. });
  896. });
  897. $imgPreview.css({
  898. left: e.pageX + 20 + 'px',
  899. top: e.pageY + 20 + 'px'
  900. });
  901. };
  902. img.onerror = function() {
  903. setImgCache(url, false);
  904. };
  905. }).on('mouseleave', 'a[data-is-link="1"]', function(e) {
  906. if ($imgPreview) $imgPreview.hide();
  907. $(document).off('mousemove.fhimg');
  908. });
  909. // 新增:全局监听,防止浮窗残留
  910. $(document).on('mousemove.fhimgcheck', function(ev) {
  911. let $target = $(ev.target).closest('a[data-is-link="1"]');
  912. if ($target.length === 0) {
  913. if ($imgPreview) $imgPreview.hide();
  914. $(document).off('mousemove.fhimg');
  915. }
  916. });
  917. };
  918. /**
  919. * 检测基本环境限制(沙盒等)
  920. * @returns {boolean}
  921. */
  922. let _checkBasicRestrictions = function() {
  923. // 检查是否在iframe中且被沙盒化
  924. if (window !== window.top) {
  925. try {
  926. // 尝试访问父窗口,如果被沙盒化会抛出异常
  927. window.parent.document;
  928. } catch (e) {
  929. // 静默处理,不输出日志
  930. return true;
  931. }
  932. }
  933. // 检查是否在受限的协议下(非chrome-extension:、http:、https:)
  934. if (location.protocol !== 'chrome-extension:' && location.protocol !== 'http:' && location.protocol !== 'https:') {
  935. // 静默处理,不输出日志
  936. return true;
  937. }
  938. return false;
  939. };
  940. /**
  941. * 初始化或获取Worker实例(异步,兼容Chrome/Edge/Firefox)
  942. * 自动检测CSP限制,如果检测到限制则回退到同步模式
  943. * @returns {Promise<Worker|null>}
  944. */
  945. let _getWorkerInstance = async function() {
  946. if (workerInstance) {
  947. return workerInstance;
  948. }
  949. // 如果已经检测到CSP限制,直接返回null,避免重复尝试
  950. if (cspRestricted) {
  951. return null;
  952. }
  953. // 检查基本环境限制(沙盒、协议等)
  954. if (_checkBasicRestrictions()) {
  955. cspRestricted = true;
  956. return null;
  957. }
  958. // 在非chrome-extension协议下,使用Blob URL方式创建Worker可能会触发CSP错误
  959. // 为了避免控制台报错,直接使用同步模式
  960. // 只有在chrome-extension协议下才使用Worker(不会有CSP限制)
  961. if (location.protocol !== 'chrome-extension:') {
  962. // 静默标记为受限,直接使用同步模式,避免触发CSP错误
  963. cspRestricted = true;
  964. return null;
  965. }
  966. // 只在chrome-extension协议下创建Worker
  967. let workerUrl = chrome.runtime.getURL('json-format/json-worker.js');
  968. // 判断是否为Firefox
  969. const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
  970. try {
  971. if (isFirefox) {
  972. try {
  973. workerInstance = new Worker(workerUrl);
  974. return workerInstance;
  975. } catch (e) {
  976. // Firefox下创建Worker失败,静默处理
  977. cspRestricted = true;
  978. return null;
  979. }
  980. } else {
  981. // Chrome/Edge在chrome-extension协议下,可以直接使用Worker URL,不需要Blob
  982. try {
  983. workerInstance = new Worker(workerUrl);
  984. return workerInstance;
  985. } catch (e) {
  986. // 创建Worker失败,静默处理
  987. cspRestricted = true;
  988. return null;
  989. }
  990. }
  991. } catch (e) {
  992. // 任何其他错误,静默标记为CSP受限并回退
  993. cspRestricted = true;
  994. workerInstance = null;
  995. return null;
  996. }
  997. };
  998. /**
  999. * 执行代码格式化
  1000. * 支持异步worker
  1001. */
  1002. let format = async function (jsonStr, skin, escapeJsonString) {
  1003. _initElements();
  1004. // 设置转义功能标志
  1005. if (escapeJsonString !== undefined) {
  1006. escapeJsonStringEnabled = escapeJsonString;
  1007. }
  1008. try {
  1009. // 先验证JSON是否有效(使用与worker一致的BigInt安全解析)
  1010. let parsedJson = _parseWithBigInt(jsonStr);
  1011. // 使用replacer保证bigint与大数字不丢精度
  1012. cachedJsonString = JSON.stringify(parsedJson, function(key, value) {
  1013. if (typeof value === 'bigint') {
  1014. return value.toString();
  1015. }
  1016. if (isBigNumberLike(value)) {
  1017. return getBigNumberDisplayString(value);
  1018. }
  1019. if (typeof value === 'number' && value.toString().includes('e')) {
  1020. return value.toLocaleString('fullwide', {useGrouping: false});
  1021. }
  1022. return value;
  1023. }, 4);
  1024. jfPre.html(htmlspecialchars(cachedJsonString));
  1025. } catch (e) {
  1026. console.error('JSON解析失败:', e);
  1027. jfContent.html(`<div class="error">JSON解析失败: ${e.message}</div>`);
  1028. return;
  1029. }
  1030. try {
  1031. // 获取Worker实例(异步)
  1032. let worker = await _getWorkerInstance();
  1033. if (worker) {
  1034. // 设置错误处理,如果Worker因为CSP等原因失败,回退到同步模式
  1035. let workerErrorHandler = function(e) {
  1036. // 静默处理,不输出日志
  1037. cspRestricted = true; // 标记为CSP受限,避免重复尝试
  1038. workerInstance = null;
  1039. formatSync(jsonStr, skin, escapeJsonString);
  1040. };
  1041. worker.onerror = workerErrorHandler;
  1042. // 设置超时,如果Worker长时间无响应,回退到同步模式
  1043. let workerTimeout = setTimeout(() => {
  1044. // 静默处理,不输出日志
  1045. if (workerInstance) {
  1046. try {
  1047. workerInstance.terminate();
  1048. } catch (e) {}
  1049. workerInstance = null;
  1050. }
  1051. formatSync(jsonStr, skin, escapeJsonString);
  1052. }, 5000);
  1053. // 设置消息处理程序
  1054. worker.onmessage = function (evt) {
  1055. clearTimeout(workerTimeout);
  1056. let msg = evt.data;
  1057. switch (msg[0]) {
  1058. case 'FORMATTING':
  1059. formattingMsg.show();
  1060. break;
  1061. case 'FORMATTED':
  1062. formattingMsg.hide();
  1063. jfContent.html(msg[1]);
  1064. _buildOptionBar();
  1065. // 事件绑定
  1066. _addEvents();
  1067. // 支持文件下载
  1068. _downloadSupport(cachedJsonString);
  1069. break;
  1070. }
  1071. };
  1072. // 发送格式化请求
  1073. try {
  1074. worker.postMessage({
  1075. jsonString: jsonStr,
  1076. skin: skin,
  1077. escapeJsonString: escapeJsonStringEnabled
  1078. });
  1079. } catch (e) {
  1080. // 如果发送消息失败(Worker可能已被CSP阻止),回退到同步模式
  1081. // 静默处理,不输出日志
  1082. cspRestricted = true; // 标记为CSP受限,避免重复尝试
  1083. clearTimeout(workerTimeout);
  1084. workerInstance = null;
  1085. formatSync(jsonStr, skin, escapeJsonString);
  1086. }
  1087. } else {
  1088. // Worker创建失败,回退到同步方式
  1089. formatSync(jsonStr, skin, escapeJsonString);
  1090. }
  1091. } catch (e) {
  1092. console.error('Worker处理失败:', e);
  1093. // 出现任何错误,回退到同步方式
  1094. formatSync(jsonStr, skin, escapeJsonString);
  1095. }
  1096. };
  1097. // 同步的方式格式化
  1098. let formatSync = function (jsonStr, skin, escapeJsonString) {
  1099. _initElements();
  1100. // 设置转义功能标志
  1101. if (escapeJsonString !== undefined) {
  1102. escapeJsonStringEnabled = escapeJsonString;
  1103. }
  1104. // 显示格式化进度
  1105. formattingMsg.show();
  1106. try {
  1107. // 先验证JSON是否有效(使用与worker一致的BigInt安全解析)
  1108. let parsedJson = _parseWithBigInt(jsonStr);
  1109. cachedJsonString = JSON.stringify(parsedJson, function(key, value) {
  1110. if (typeof value === 'bigint') {
  1111. return value.toString();
  1112. }
  1113. if (isBigNumberLike(value)) {
  1114. return getBigNumberDisplayString(value);
  1115. }
  1116. if (typeof value === 'number' && value.toString().includes('e')) {
  1117. return value.toLocaleString('fullwide', {useGrouping: false});
  1118. }
  1119. return value;
  1120. }, 4);
  1121. // 设置原始JSON内容到jfPre(用于元数据按钮)
  1122. jfPre.html(htmlspecialchars(cachedJsonString));
  1123. // 使用完整的JSON美化功能
  1124. let formattedHtml = formatJsonToHtml(parsedJson, skin);
  1125. // 创建正确的HTML结构:jfContent > formattedJson
  1126. let formattedJsonDiv = $('<div id="formattedJson"></div>');
  1127. formattedJsonDiv.html(formattedHtml);
  1128. jfContent.html(formattedJsonDiv);
  1129. // 隐藏进度提示
  1130. formattingMsg.hide();
  1131. // 构建操作栏
  1132. _buildOptionBar();
  1133. // 事件绑定
  1134. _addEvents();
  1135. // 支持文件下载
  1136. _downloadSupport(cachedJsonString);
  1137. return;
  1138. } catch (e) {
  1139. console.error('JSON格式化失败:', e);
  1140. jfContent.html(`<div class="error">JSON格式化失败: ${e.message}</div>`);
  1141. // 隐藏进度提示
  1142. formattingMsg.hide();
  1143. }
  1144. };
  1145. // 与 worker 保持一致的 BigInt 安全解析:
  1146. // 1) 给可能的大整数加标记;2) 使用reviver还原为原生BigInt
  1147. let _parseWithBigInt = function(text) {
  1148. // 先解析JSON,然后在对象层面处理大整数
  1149. // 这样可以避免在字符串内容中错误地匹配数字
  1150. try {
  1151. // 直接解析,如果成功则不需要BigInt处理
  1152. return JSON.parse(text);
  1153. } catch (e) {
  1154. // 如果解析失败,可能是因为大整数导致的精度问题
  1155. // 使用更安全的方式:只在非字符串上下文中标记大整数
  1156. // 通过状态机跟踪是否在字符串内部
  1157. let inString = false;
  1158. let escape = false;
  1159. let marked = '';
  1160. for (let i = 0; i < text.length; i++) {
  1161. const char = text[i];
  1162. if (escape) {
  1163. marked += char;
  1164. escape = false;
  1165. continue;
  1166. }
  1167. if (char === '\\') {
  1168. marked += char;
  1169. escape = true;
  1170. continue;
  1171. }
  1172. if (char === '"') {
  1173. inString = !inString;
  1174. marked += char;
  1175. continue;
  1176. }
  1177. marked += char;
  1178. }
  1179. // 只在非字符串上下文中替换大整数
  1180. // 使用更精确的正则,确保不在字符串内
  1181. const result = text.replace(/([:,\[]\s*)(-?\d{16,})(\s*)(?=[,\]\}])/g, function(match, prefix, number, spaces, offset) {
  1182. // 检查这个位置是否在字符串内
  1183. let inStr = false;
  1184. let esc = false;
  1185. for (let i = 0; i < offset; i++) {
  1186. if (esc) {
  1187. esc = false;
  1188. continue;
  1189. }
  1190. if (text[i] === '\\') {
  1191. esc = true;
  1192. continue;
  1193. }
  1194. if (text[i] === '"') {
  1195. inStr = !inStr;
  1196. }
  1197. }
  1198. // 如果在字符串内,不替换
  1199. if (inStr) {
  1200. return match;
  1201. }
  1202. return prefix + '"__BigInt__' + number + '"' + spaces;
  1203. });
  1204. return JSON.parse(result, function(key, value) {
  1205. if (typeof value === 'string' && value.indexOf('__BigInt__') === 0) {
  1206. try {
  1207. return BigInt(value.slice(10));
  1208. } catch (e) {
  1209. return value.slice(10);
  1210. }
  1211. }
  1212. return value;
  1213. });
  1214. }
  1215. };
  1216. // 工具函数:获取或创建唯一图片预览浮窗节点
  1217. function getOrCreateImgPreview() {
  1218. let $img = $('#fh-img-preview');
  1219. if (!$img.length) {
  1220. $img = $('<div id="fh-img-preview" style="position:absolute;z-index:999999;border:1px solid #ccc;background:#fff;padding:4px;box-shadow:0 2px 8px #0002;pointer-events:none;"><img style="max-width:300px;max-height:200px;display:block;"></div>').appendTo('body');
  1221. }
  1222. return $img;
  1223. }
  1224. // 格式化JSON为HTML(同步版本)
  1225. function formatJsonToHtml(json, skin) {
  1226. return createNode(json).getHTML();
  1227. }
  1228. // 创建节点 - 直接复用webworker中的完整逻辑
  1229. function createNode(value) {
  1230. let node = {
  1231. type: getType(value),
  1232. value: value,
  1233. children: [],
  1234. getHTML: function() {
  1235. switch(this.type) {
  1236. case 'string':
  1237. // 判断原始字符串是否为URL
  1238. if (isUrl(this.value)) {
  1239. // 用JSON.stringify保证转义符显示,内容包裹在<a>里
  1240. return '<div class="item item-line"><span class="string"><a href="'
  1241. + htmlspecialchars(this.value) + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + htmlspecialchars(this.value) + '">'
  1242. + htmlspecialchars(JSON.stringify(this.value)) + '</a></span></div>';
  1243. } else {
  1244. // 检测字符串是否是有效的JSON(用于转义功能)
  1245. // 当转义功能开启时,如果字符串是有效的JSON,就格式化显示
  1246. if (escapeJsonStringEnabled) {
  1247. const strValue = String(this.value);
  1248. // 检查字符串是否看起来像JSON(以[或{开头,以]或}结尾)
  1249. const trimmed = strValue.trim();
  1250. if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
  1251. (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
  1252. try {
  1253. // 尝试解析为JSON,使用全局的 JSON.parse(已被 json-bigint.js 覆盖)
  1254. const parsed = JSON.parse(strValue);
  1255. // 如果解析成功且是对象或数组,格式化显示
  1256. if (typeof parsed === 'object' && parsed !== null) {
  1257. const nestedNode = createNode(parsed);
  1258. // 获取嵌套JSON的完整HTML(完全展开)
  1259. let nestedHTML = nestedNode.getHTML();
  1260. // 移除外层的item容器div,只保留内部内容
  1261. nestedHTML = nestedHTML.replace(/^<div class="item[^"]*">/, '').replace(/<\/div>$/, '');
  1262. // 返回格式化的JSON结构,但保持在外层的字符串容器中
  1263. // 使用block显示,确保完全展开
  1264. return '<div class="item item-line"><span class="string">' +
  1265. '<span class="quote">"</span>' +
  1266. '<div class="string-json-nested" style="display:block;margin-left:0;padding-left:0;">' +
  1267. nestedHTML +
  1268. '</div>' +
  1269. '<span class="quote">"</span>' +
  1270. '</span></div>';
  1271. }
  1272. } catch (e) {
  1273. // 解析失败,按普通字符串处理
  1274. }
  1275. }
  1276. }
  1277. return '<div class="item item-line"><span class="string">' + formatStringValue(JSON.stringify(this.value)) + '</span></div>';
  1278. }
  1279. case 'number':
  1280. // 确保大数字不使用科学计数法
  1281. let numStr = typeof this.value === 'number' && this.value.toString().includes('e')
  1282. ? this.value.toLocaleString('fullwide', {useGrouping: false})
  1283. : this.value;
  1284. return '<div class="item item-line"><span class="number">' +
  1285. numStr +
  1286. '</span></div>';
  1287. case 'bigint':
  1288. // 对BigInt类型特殊处理,只显示数字,不添加n后缀
  1289. return '<div class="item item-line"><span class="number">' +
  1290. getBigNumberDisplayString(this.value) +
  1291. '</span></div>';
  1292. case 'boolean':
  1293. return '<div class="item item-line"><span class="bool">' +
  1294. this.value +
  1295. '</span></div>';
  1296. case 'null':
  1297. return '<div class="item item-line"><span class="null">null</span></div>';
  1298. case 'object':
  1299. return this.getObjectHTML();
  1300. case 'array':
  1301. return this.getArrayHTML();
  1302. default:
  1303. return '';
  1304. }
  1305. },
  1306. getObjectHTML: function() {
  1307. if (!this.value || Object.keys(this.value).length === 0) {
  1308. return '<div class="item item-object"><span class="brace">{</span><span class="brace">}</span></div>';
  1309. }
  1310. let html = '<div class="item item-object">' +
  1311. '<span class="expand"></span>' +
  1312. '<span class="brace">{</span>' +
  1313. '<span class="ellipsis"></span>' +
  1314. '<div class="kv-list">';
  1315. let keys = Object.keys(this.value);
  1316. keys.forEach((key, index) => {
  1317. let prop = this.value[key];
  1318. let childNode = createNode(prop);
  1319. // 判断子节点是否为对象或数组,决定是否加item-block
  1320. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1321. html += '<div class="' + itemClass + '">';
  1322. // 如果值是对象或数组,在key前面添加展开按钮
  1323. if (childNode.type === 'object' || childNode.type === 'array') {
  1324. html += '<span class="expand"></span>';
  1325. }
  1326. html += '<span class="quote">"</span>' +
  1327. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1328. '<span class="quote">"</span>' +
  1329. '<span class="colon">: </span>';
  1330. // 添加值
  1331. if (childNode.type === 'object' || childNode.type === 'array') {
  1332. html += childNode.getInlineHTMLWithoutExpand();
  1333. } else {
  1334. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1335. }
  1336. // 如果不是最后一个属性,添加逗号
  1337. if (index < keys.length - 1) {
  1338. html += '<span class="comma">,</span>';
  1339. }
  1340. html += '</div>';
  1341. });
  1342. html += '</div><span class="brace">}</span></div>';
  1343. return html;
  1344. },
  1345. getArrayHTML: function() {
  1346. if (!this.value || this.value.length === 0) {
  1347. return '<div class="item item-array"><span class="brace">[</span><span class="brace">]</span></div>';
  1348. }
  1349. let html = '<div class="item item-array">' +
  1350. '<span class="expand"></span>' +
  1351. '<span class="brace">[</span>' +
  1352. '<span class="ellipsis"></span>' +
  1353. '<div class="kv-list item-array-container">';
  1354. this.value.forEach((item, index) => {
  1355. let childNode = createNode(item);
  1356. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1357. // 如果数组元素是对象或数组,在前面添加展开按钮
  1358. if (childNode.type === 'object' || childNode.type === 'array') {
  1359. html += '<span class="expand"></span>';
  1360. html += childNode.getInlineHTMLWithoutExpand();
  1361. } else {
  1362. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1363. }
  1364. // 如果不是最后一个元素,添加逗号
  1365. if (index < this.value.length - 1) {
  1366. html += '<span class="comma">,</span>';
  1367. }
  1368. html += '</div>';
  1369. });
  1370. html += '</div><span class="brace">]</span></div>';
  1371. return html;
  1372. },
  1373. // 新增内联HTML方法,用于在同一行显示开始大括号/方括号
  1374. getInlineHTML: function() {
  1375. switch(this.type) {
  1376. case 'object':
  1377. return this.getInlineObjectHTML();
  1378. case 'array':
  1379. return this.getInlineArrayHTML();
  1380. default:
  1381. return this.getHTML();
  1382. }
  1383. },
  1384. // 新增不包含展开按钮的内联HTML方法
  1385. getInlineHTMLWithoutExpand: function() {
  1386. switch(this.type) {
  1387. case 'object':
  1388. return this.getInlineObjectHTMLWithoutExpand();
  1389. case 'array':
  1390. return this.getInlineArrayHTMLWithoutExpand();
  1391. default:
  1392. return this.getHTML();
  1393. }
  1394. },
  1395. getInlineObjectHTML: function() {
  1396. if (!this.value || Object.keys(this.value).length === 0) {
  1397. return '<span class="brace">{</span><span class="brace">}</span>';
  1398. }
  1399. let html = '<span class="brace">{</span>' +
  1400. '<span class="expand"></span>' +
  1401. '<span class="ellipsis"></span>' +
  1402. '<div class="kv-list">';
  1403. let keys = Object.keys(this.value);
  1404. keys.forEach((key, index) => {
  1405. let prop = this.value[key];
  1406. let childNode = createNode(prop);
  1407. // 判断子节点是否为对象或数组,决定是否加item-block
  1408. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1409. html += '<div class="' + itemClass + '">';
  1410. if (childNode.type === 'object' || childNode.type === 'array') {
  1411. html += '<span class="expand"></span>';
  1412. }
  1413. html += '<span class="quote">"</span>' +
  1414. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1415. '<span class="quote">"</span>' +
  1416. '<span class="colon">: </span>';
  1417. if (childNode.type === 'object' || childNode.type === 'array') {
  1418. html += childNode.getInlineHTMLWithoutExpand();
  1419. } else {
  1420. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1421. }
  1422. if (index < keys.length - 1) {
  1423. html += '<span class="comma">,</span>';
  1424. }
  1425. html += '</div>';
  1426. });
  1427. html += '</div><span class="brace">}</span>';
  1428. return html;
  1429. },
  1430. getInlineArrayHTML: function() {
  1431. if (!this.value || this.value.length === 0) {
  1432. return '<span class="brace">[</span><span class="brace">]</span>';
  1433. }
  1434. let html = '<span class="brace">[</span>' +
  1435. '<span class="expand"></span>' +
  1436. '<span class="ellipsis"></span>' +
  1437. '<div class="kv-list item-array-container">';
  1438. this.value.forEach((item, index) => {
  1439. let childNode = createNode(item);
  1440. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1441. // 如果数组元素是对象或数组,在前面添加展开按钮
  1442. if (childNode.type === 'object' || childNode.type === 'array') {
  1443. html += '<span class="expand"></span>';
  1444. html += childNode.getInlineHTMLWithoutExpand();
  1445. } else {
  1446. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1447. }
  1448. // 如果不是最后一个元素,添加逗号
  1449. if (index < this.value.length - 1) {
  1450. html += '<span class="comma">,</span>';
  1451. }
  1452. html += '</div>';
  1453. });
  1454. html += '</div><span class="brace">]</span>';
  1455. return html;
  1456. },
  1457. getInlineObjectHTMLWithoutExpand: function() {
  1458. if (!this.value || Object.keys(this.value).length === 0) {
  1459. return '<span class="brace">{</span><span class="brace">}</span>';
  1460. }
  1461. let html = '<span class="brace">{</span>' +
  1462. '<span class="ellipsis"></span>' +
  1463. '<div class="kv-list">';
  1464. let keys = Object.keys(this.value);
  1465. keys.forEach((key, index) => {
  1466. let prop = this.value[key];
  1467. let childNode = createNode(prop);
  1468. // 判断子节点是否为对象或数组,决定是否加item-block
  1469. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1470. html += '<div class="' + itemClass + '">';
  1471. if (childNode.type === 'object' || childNode.type === 'array') {
  1472. html += '<span class="expand"></span>';
  1473. }
  1474. html += '<span class="quote">"</span>' +
  1475. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1476. '<span class="quote">"</span>' +
  1477. '<span class="colon">: </span>';
  1478. if (childNode.type === 'object' || childNode.type === 'array') {
  1479. html += childNode.getInlineHTMLWithoutExpand();
  1480. } else {
  1481. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1482. }
  1483. if (index < keys.length - 1) {
  1484. html += '<span class="comma">,</span>';
  1485. }
  1486. html += '</div>';
  1487. });
  1488. html += '</div><span class="brace">}</span>';
  1489. return html;
  1490. },
  1491. getInlineArrayHTMLWithoutExpand: function() {
  1492. if (!this.value || this.value.length === 0) {
  1493. return '<span class="brace">[</span><span class="brace">]</span>';
  1494. }
  1495. let html = '<span class="brace">[</span>' +
  1496. '<span class="ellipsis"></span>' +
  1497. '<div class="kv-list item-array-container">';
  1498. this.value.forEach((item, index) => {
  1499. let childNode = createNode(item);
  1500. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1501. // 确保所有类型的数组元素都能正确处理
  1502. if (childNode.type === 'object' || childNode.type === 'array') {
  1503. html += '<span class="expand"></span>';
  1504. html += childNode.getInlineHTMLWithoutExpand();
  1505. } else {
  1506. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1507. }
  1508. // 如果不是最后一个元素,添加逗号
  1509. if (index < this.value.length - 1) {
  1510. html += '<span class="comma">,</span>';
  1511. }
  1512. html += '</div>';
  1513. });
  1514. html += '</div><span class="brace">]</span>';
  1515. return html;
  1516. }
  1517. };
  1518. return node;
  1519. }
  1520. // 获取值的类型
  1521. function getType(value) {
  1522. if (value === null) return 'null';
  1523. if (typeof value === 'bigint') return 'bigint';
  1524. if (typeof value === 'object') {
  1525. if (isBigNumberLike(value)) {
  1526. return 'bigint'; // 将 BigNumber 对象也当作 bigint 处理
  1527. }
  1528. if (Array.isArray(value)) return 'array';
  1529. return 'object';
  1530. }
  1531. return typeof value;
  1532. }
  1533. // 判断是否为URL
  1534. function isUrl(str) {
  1535. if (typeof str !== 'string') return false;
  1536. const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
  1537. return urlRegex.test(str);
  1538. }
  1539. // 格式化字符串值,如果是URL则转换为链接
  1540. function formatStringValue(str) {
  1541. // URL正则表达式,匹配 http/https/ftp 协议的URL
  1542. const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
  1543. if (urlRegex.test(str)) {
  1544. // 如果是URL,转换为链接
  1545. const escapedUrl = htmlspecialchars(str);
  1546. return '<a href="' + escapedUrl + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + escapedUrl + '">' + htmlspecialchars(str) + '</a>';
  1547. } else {
  1548. // 直接显示解析后的字符串内容,不需要重新转义
  1549. // 这样可以保持用户原始输入的意图
  1550. return htmlspecialchars(str);
  1551. }
  1552. }
  1553. function isBigNumberLike(value) {
  1554. return value && typeof value === 'object' &&
  1555. typeof value.s === 'number' &&
  1556. typeof value.e === 'number' &&
  1557. Array.isArray(value.c);
  1558. }
  1559. function getBigNumberDisplayString(value) {
  1560. if (typeof value === 'bigint') {
  1561. return value.toString();
  1562. }
  1563. if (!isBigNumberLike(value)) {
  1564. return String(value);
  1565. }
  1566. const direct = tryConvertBigNumberToString(value);
  1567. if (direct) {
  1568. return direct;
  1569. }
  1570. return rebuildBigNumberFromParts(value);
  1571. }
  1572. function tryConvertBigNumberToString(value) {
  1573. const nativeToString = value && value.toString;
  1574. if (typeof nativeToString === 'function' && nativeToString !== Object.prototype.toString) {
  1575. try {
  1576. const result = nativeToString.call(value);
  1577. if (typeof result === 'string' && result !== '[object Object]') {
  1578. return result;
  1579. }
  1580. } catch (e) {}
  1581. }
  1582. const ctor = getAvailableBigNumberCtor();
  1583. if (ctor && typeof Object.setPrototypeOf === 'function') {
  1584. try {
  1585. if (!(value instanceof ctor)) {
  1586. Object.setPrototypeOf(value, ctor.prototype);
  1587. }
  1588. if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) {
  1589. const result = value.toString();
  1590. if (typeof result === 'string' && result !== '[object Object]') {
  1591. return result;
  1592. }
  1593. }
  1594. } catch (e) {}
  1595. }
  1596. return null;
  1597. }
  1598. function rebuildBigNumberFromParts(value) {
  1599. const sign = value.s < 0 ? '-' : '';
  1600. const CHUNK_SIZE = 14;
  1601. let digits = '';
  1602. for (let i = 0; i < value.c.length; i++) {
  1603. let chunkStr = Math.abs(value.c[i]).toString();
  1604. if (i > 0) {
  1605. chunkStr = chunkStr.padStart(CHUNK_SIZE, '0');
  1606. }
  1607. digits += chunkStr;
  1608. }
  1609. digits = digits.replace(/^0+/, '') || '0';
  1610. const decimalIndex = value.e + 1;
  1611. if (decimalIndex <= 0) {
  1612. const zeros = '0'.repeat(Math.abs(decimalIndex));
  1613. let fraction = zeros + digits;
  1614. fraction = fraction.replace(/0+$/, '');
  1615. if (!fraction) {
  1616. return sign + '0';
  1617. }
  1618. return sign + '0.' + fraction;
  1619. }
  1620. if (decimalIndex >= digits.length) {
  1621. return sign + digits + '0'.repeat(decimalIndex - digits.length);
  1622. }
  1623. const intPart = digits.slice(0, decimalIndex);
  1624. let fracPart = digits.slice(decimalIndex).replace(/0+$/, '');
  1625. if (!fracPart) {
  1626. return sign + intPart;
  1627. }
  1628. return sign + intPart + '.' + fracPart;
  1629. }
  1630. function getAvailableBigNumberCtor() {
  1631. if (typeof JSON !== 'undefined' && typeof JSON.BigNumber === 'function') {
  1632. return JSON.BigNumber;
  1633. }
  1634. if (typeof BigNumber === 'function') {
  1635. return BigNumber;
  1636. }
  1637. return null;
  1638. }
  1639. return {
  1640. format: format,
  1641. formatSync: formatSync,
  1642. setEscapeEnabled: function(enabled) {
  1643. escapeJsonStringEnabled = enabled;
  1644. }
  1645. }
  1646. })();