format-lib.js 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553
  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是否有效
  983. let parsedJson = JSON.parse(jsonStr);
  984. cachedJsonString = JSON.stringify(parsedJson, null, 4);
  985. jfPre.html(htmlspecialchars(cachedJsonString));
  986. } catch (e) {
  987. console.error('JSON解析失败:', e);
  988. jfContent.html(`<div class="error">JSON解析失败: ${e.message}</div>`);
  989. return;
  990. }
  991. try {
  992. // 获取Worker实例(异步)
  993. let worker = await _getWorkerInstance();
  994. if (worker) {
  995. // 设置消息处理程序
  996. worker.onmessage = function (evt) {
  997. let msg = evt.data;
  998. switch (msg[0]) {
  999. case 'FORMATTING':
  1000. formattingMsg.show();
  1001. break;
  1002. case 'FORMATTED':
  1003. formattingMsg.hide();
  1004. jfContent.html(msg[1]);
  1005. _buildOptionBar();
  1006. // 事件绑定
  1007. _addEvents();
  1008. // 支持文件下载
  1009. _downloadSupport(cachedJsonString);
  1010. break;
  1011. }
  1012. };
  1013. // 发送格式化请求
  1014. worker.postMessage({
  1015. jsonString: jsonStr,
  1016. skin: skin
  1017. });
  1018. } else {
  1019. // Worker创建失败,回退到同步方式
  1020. formatSync(jsonStr, skin);
  1021. }
  1022. } catch (e) {
  1023. console.error('Worker处理失败:', e);
  1024. // 出现任何错误,回退到同步方式
  1025. formatSync(jsonStr, skin);
  1026. }
  1027. };
  1028. // 同步的方式格式化
  1029. let formatSync = function (jsonStr, skin) {
  1030. _initElements();
  1031. // 显示格式化进度
  1032. formattingMsg.show();
  1033. try {
  1034. // 先验证JSON是否有效
  1035. let parsedJson = JSON.parse(jsonStr);
  1036. cachedJsonString = JSON.stringify(parsedJson, null, 4);
  1037. // 设置原始JSON内容到jfPre(用于元数据按钮)
  1038. jfPre.html(htmlspecialchars(cachedJsonString));
  1039. // 使用完整的JSON美化功能
  1040. let formattedHtml = formatJsonToHtml(parsedJson, skin);
  1041. // 创建正确的HTML结构:jfContent > formattedJson
  1042. let formattedJsonDiv = $('<div id="formattedJson"></div>');
  1043. formattedJsonDiv.html(formattedHtml);
  1044. jfContent.html(formattedJsonDiv);
  1045. // 隐藏进度提示
  1046. formattingMsg.hide();
  1047. // 构建操作栏
  1048. _buildOptionBar();
  1049. // 事件绑定
  1050. _addEvents();
  1051. // 支持文件下载
  1052. _downloadSupport(cachedJsonString);
  1053. return;
  1054. } catch (e) {
  1055. console.error('JSON格式化失败:', e);
  1056. jfContent.html(`<div class="error">JSON格式化失败: ${e.message}</div>`);
  1057. // 隐藏进度提示
  1058. formattingMsg.hide();
  1059. }
  1060. };
  1061. // 工具函数:获取或创建唯一图片预览浮窗节点
  1062. function getOrCreateImgPreview() {
  1063. let $img = $('#fh-img-preview');
  1064. if (!$img.length) {
  1065. $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');
  1066. }
  1067. return $img;
  1068. }
  1069. // 格式化JSON为HTML(同步版本)
  1070. function formatJsonToHtml(json, skin) {
  1071. return createNode(json).getHTML();
  1072. }
  1073. // 创建节点 - 直接复用webworker中的完整逻辑
  1074. function createNode(value) {
  1075. let node = {
  1076. type: getType(value),
  1077. value: value,
  1078. children: [],
  1079. getHTML: function() {
  1080. switch(this.type) {
  1081. case 'string':
  1082. // 判断原始字符串是否为URL
  1083. if (isUrl(this.value)) {
  1084. // 用JSON.stringify保证转义符显示,内容包裹在<a>里
  1085. return '<div class="item item-line"><span class="string"><a href="'
  1086. + htmlspecialchars(this.value) + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + htmlspecialchars(this.value) + '">'
  1087. + htmlspecialchars(JSON.stringify(this.value)) + '</a></span></div>';
  1088. } else {
  1089. return '<div class="item item-line"><span class="string">' + formatStringValue(JSON.stringify(this.value)) + '</span></div>';
  1090. }
  1091. case 'number':
  1092. // 确保大数字不使用科学计数法
  1093. let numStr = typeof this.value === 'number' && this.value.toString().includes('e')
  1094. ? this.value.toLocaleString('fullwide', {useGrouping: false})
  1095. : this.value;
  1096. return '<div class="item item-line"><span class="number">' +
  1097. numStr +
  1098. '</span></div>';
  1099. case 'bigint':
  1100. // 对BigInt类型特殊处理,只显示数字,不添加n后缀
  1101. return '<div class="item item-line"><span class="number">' +
  1102. this.value.toString() +
  1103. '</span></div>';
  1104. case 'boolean':
  1105. return '<div class="item item-line"><span class="bool">' +
  1106. this.value +
  1107. '</span></div>';
  1108. case 'null':
  1109. return '<div class="item item-line"><span class="null">null</span></div>';
  1110. case 'object':
  1111. return this.getObjectHTML();
  1112. case 'array':
  1113. return this.getArrayHTML();
  1114. default:
  1115. return '';
  1116. }
  1117. },
  1118. getObjectHTML: function() {
  1119. if (!this.value || Object.keys(this.value).length === 0) {
  1120. return '<div class="item item-object"><span class="brace">{</span><span class="brace">}</span></div>';
  1121. }
  1122. let html = '<div class="item item-object">' +
  1123. '<span class="expand"></span>' +
  1124. '<span class="brace">{</span>' +
  1125. '<span class="ellipsis"></span>' +
  1126. '<div class="kv-list">';
  1127. let keys = Object.keys(this.value);
  1128. keys.forEach((key, index) => {
  1129. let prop = this.value[key];
  1130. let childNode = createNode(prop);
  1131. // 判断子节点是否为对象或数组,决定是否加item-block
  1132. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1133. html += '<div class="' + itemClass + '">';
  1134. // 如果值是对象或数组,在key前面添加展开按钮
  1135. if (childNode.type === 'object' || childNode.type === 'array') {
  1136. html += '<span class="expand"></span>';
  1137. }
  1138. html += '<span class="quote">"</span>' +
  1139. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1140. '<span class="quote">"</span>' +
  1141. '<span class="colon">: </span>';
  1142. // 添加值
  1143. if (childNode.type === 'object' || childNode.type === 'array') {
  1144. html += childNode.getInlineHTMLWithoutExpand();
  1145. } else {
  1146. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1147. }
  1148. // 如果不是最后一个属性,添加逗号
  1149. if (index < keys.length - 1) {
  1150. html += '<span class="comma">,</span>';
  1151. }
  1152. html += '</div>';
  1153. });
  1154. html += '</div><span class="brace">}</span></div>';
  1155. return html;
  1156. },
  1157. getArrayHTML: function() {
  1158. if (!this.value || this.value.length === 0) {
  1159. return '<div class="item item-array"><span class="brace">[</span><span class="brace">]</span></div>';
  1160. }
  1161. let html = '<div class="item item-array">' +
  1162. '<span class="expand"></span>' +
  1163. '<span class="brace">[</span>' +
  1164. '<span class="ellipsis"></span>' +
  1165. '<div class="kv-list item-array-container">';
  1166. this.value.forEach((item, index) => {
  1167. let childNode = createNode(item);
  1168. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1169. // 如果数组元素是对象或数组,在前面添加展开按钮
  1170. if (childNode.type === 'object' || childNode.type === 'array') {
  1171. html += '<span class="expand"></span>';
  1172. html += childNode.getInlineHTMLWithoutExpand();
  1173. } else {
  1174. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1175. }
  1176. // 如果不是最后一个元素,添加逗号
  1177. if (index < this.value.length - 1) {
  1178. html += '<span class="comma">,</span>';
  1179. }
  1180. html += '</div>';
  1181. });
  1182. html += '</div><span class="brace">]</span></div>';
  1183. return html;
  1184. },
  1185. // 新增内联HTML方法,用于在同一行显示开始大括号/方括号
  1186. getInlineHTML: function() {
  1187. switch(this.type) {
  1188. case 'object':
  1189. return this.getInlineObjectHTML();
  1190. case 'array':
  1191. return this.getInlineArrayHTML();
  1192. default:
  1193. return this.getHTML();
  1194. }
  1195. },
  1196. // 新增不包含展开按钮的内联HTML方法
  1197. getInlineHTMLWithoutExpand: function() {
  1198. switch(this.type) {
  1199. case 'object':
  1200. return this.getInlineObjectHTMLWithoutExpand();
  1201. case 'array':
  1202. return this.getInlineArrayHTMLWithoutExpand();
  1203. default:
  1204. return this.getHTML();
  1205. }
  1206. },
  1207. getInlineObjectHTML: function() {
  1208. if (!this.value || Object.keys(this.value).length === 0) {
  1209. return '<span class="brace">{</span><span class="brace">}</span>';
  1210. }
  1211. let html = '<span class="brace">{</span>' +
  1212. '<span class="expand"></span>' +
  1213. '<span class="ellipsis"></span>' +
  1214. '<div class="kv-list">';
  1215. let keys = Object.keys(this.value);
  1216. keys.forEach((key, index) => {
  1217. let prop = this.value[key];
  1218. let childNode = createNode(prop);
  1219. // 判断子节点是否为对象或数组,决定是否加item-block
  1220. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1221. html += '<div class="' + itemClass + '">';
  1222. if (childNode.type === 'object' || childNode.type === 'array') {
  1223. html += '<span class="expand"></span>';
  1224. }
  1225. html += '<span class="quote">"</span>' +
  1226. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1227. '<span class="quote">"</span>' +
  1228. '<span class="colon">: </span>';
  1229. if (childNode.type === 'object' || childNode.type === 'array') {
  1230. html += childNode.getInlineHTMLWithoutExpand();
  1231. } else {
  1232. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1233. }
  1234. if (index < keys.length - 1) {
  1235. html += '<span class="comma">,</span>';
  1236. }
  1237. html += '</div>';
  1238. });
  1239. html += '</div><span class="brace">}</span>';
  1240. return html;
  1241. },
  1242. getInlineArrayHTML: function() {
  1243. if (!this.value || 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 item-array-container">';
  1250. this.value.forEach((item, index) => {
  1251. let childNode = createNode(item);
  1252. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1253. // 如果数组元素是对象或数组,在前面添加展开按钮
  1254. if (childNode.type === 'object' || childNode.type === 'array') {
  1255. html += '<span class="expand"></span>';
  1256. html += childNode.getInlineHTMLWithoutExpand();
  1257. } else {
  1258. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1259. }
  1260. // 如果不是最后一个元素,添加逗号
  1261. if (index < this.value.length - 1) {
  1262. html += '<span class="comma">,</span>';
  1263. }
  1264. html += '</div>';
  1265. });
  1266. html += '</div><span class="brace">]</span>';
  1267. return html;
  1268. },
  1269. getInlineObjectHTMLWithoutExpand: function() {
  1270. if (!this.value || Object.keys(this.value).length === 0) {
  1271. return '<span class="brace">{</span><span class="brace">}</span>';
  1272. }
  1273. let html = '<span class="brace">{</span>' +
  1274. '<span class="ellipsis"></span>' +
  1275. '<div class="kv-list">';
  1276. let keys = Object.keys(this.value);
  1277. keys.forEach((key, index) => {
  1278. let prop = this.value[key];
  1279. let childNode = createNode(prop);
  1280. // 判断子节点是否为对象或数组,决定是否加item-block
  1281. let itemClass = (childNode.type === 'object' || childNode.type === 'array') ? 'item item-block' : 'item';
  1282. html += '<div class="' + itemClass + '">';
  1283. if (childNode.type === 'object' || childNode.type === 'array') {
  1284. html += '<span class="expand"></span>';
  1285. }
  1286. html += '<span class="quote">"</span>' +
  1287. '<span class="key">' + htmlspecialchars(key) + '</span>' +
  1288. '<span class="quote">"</span>' +
  1289. '<span class="colon">: </span>';
  1290. if (childNode.type === 'object' || childNode.type === 'array') {
  1291. html += childNode.getInlineHTMLWithoutExpand();
  1292. } else {
  1293. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1294. }
  1295. if (index < keys.length - 1) {
  1296. html += '<span class="comma">,</span>';
  1297. }
  1298. html += '</div>';
  1299. });
  1300. html += '</div><span class="brace">}</span>';
  1301. return html;
  1302. },
  1303. getInlineArrayHTMLWithoutExpand: function() {
  1304. if (!this.value || this.value.length === 0) {
  1305. return '<span class="brace">[</span><span class="brace">]</span>';
  1306. }
  1307. let html = '<span class="brace">[</span>' +
  1308. '<span class="ellipsis"></span>' +
  1309. '<div class="kv-list item-array-container">';
  1310. this.value.forEach((item, index) => {
  1311. let childNode = createNode(item);
  1312. html += '<div class="item item-block item-array-element" data-array-index="' + index + '">';
  1313. // 确保所有类型的数组元素都能正确处理
  1314. if (childNode.type === 'object' || childNode.type === 'array') {
  1315. html += '<span class="expand"></span>';
  1316. html += childNode.getInlineHTMLWithoutExpand();
  1317. } else {
  1318. html += childNode.getHTML().replace(/^<div class="item item-line">/, '').replace(/<\/div>$/, '');
  1319. }
  1320. // 如果不是最后一个元素,添加逗号
  1321. if (index < this.value.length - 1) {
  1322. html += '<span class="comma">,</span>';
  1323. }
  1324. html += '</div>';
  1325. });
  1326. html += '</div><span class="brace">]</span>';
  1327. return html;
  1328. }
  1329. };
  1330. return node;
  1331. }
  1332. // 获取值的类型
  1333. function getType(value) {
  1334. if (value === null) return 'null';
  1335. if (typeof value === 'bigint') return 'bigint';
  1336. if (Array.isArray(value)) return 'array';
  1337. if (typeof value === 'object') return 'object';
  1338. return typeof value;
  1339. }
  1340. // 判断是否为URL
  1341. function isUrl(str) {
  1342. if (typeof str !== 'string') return false;
  1343. const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
  1344. return urlRegex.test(str);
  1345. }
  1346. // 格式化字符串值,如果是URL则转换为链接
  1347. function formatStringValue(str) {
  1348. // URL正则表达式,匹配 http/https/ftp 协议的URL
  1349. const urlRegex = /^(https?:\/\/|ftp:\/\/)[^\s<>"'\\]+$/i;
  1350. if (urlRegex.test(str)) {
  1351. // 如果是URL,转换为链接
  1352. const escapedUrl = htmlspecialchars(str);
  1353. return '<a href="' + escapedUrl + '" target="_blank" rel="noopener noreferrer" data-is-link="1" data-link-url="' + escapedUrl + '">' + htmlspecialchars(str) + '</a>';
  1354. } else {
  1355. // 直接显示解析后的字符串内容,不需要重新转义
  1356. // 这样可以保持用户原始输入的意图
  1357. return htmlspecialchars(str);
  1358. }
  1359. }
  1360. return {
  1361. format: format,
  1362. formatSync: formatSync
  1363. }
  1364. })();