format-lib.js 61 KB

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