format-lib.js 73 KB

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