format-lib.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  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. return str;
  121. };
  122. /**
  123. * 直接下载,能解决中文乱码
  124. * @param content
  125. * @private
  126. */
  127. let _downloadSupport = function (content) {
  128. // 下载链接
  129. let dt = (new Date()).format('yyyyMMddHHmmss');
  130. let blob = new Blob([content], {type: 'application/octet-stream'});
  131. let button = $('<button class="xjf-btn xjf-btn-right">下载JSON</button>').appendTo('#optionBar');
  132. if (typeof chrome === 'undefined' || !chrome.permissions) {
  133. button.click(function (e) {
  134. let aLink = $('#aLinkDownload');
  135. if (!aLink[0]) {
  136. aLink = $('<a id="aLinkDownload" target="_blank" title="保存到本地">下载JSON数据</a>').appendTo('body');
  137. aLink.attr('download', 'FeHelper-' + dt + '.json');
  138. aLink.attr('href', URL.createObjectURL(blob));
  139. }
  140. aLink[0].click();
  141. });
  142. } else {
  143. button.click(function (e) {
  144. // 请求权限
  145. chrome.permissions.request({
  146. permissions: ['downloads']
  147. }, (granted) => {
  148. if (granted) {
  149. chrome.downloads.download({
  150. url: URL.createObjectURL(blob),
  151. saveAs: true,
  152. conflictAction: 'overwrite',
  153. filename: 'FeHelper-' + dt + '.json'
  154. });
  155. } else {
  156. toast('必须接受授权,才能正常下载!');
  157. }
  158. });
  159. });
  160. }
  161. };
  162. /**
  163. * chrome 下复制到剪贴板
  164. * @param text
  165. */
  166. let _copyToClipboard = function (text) {
  167. let input = document.createElement('textarea');
  168. input.style.position = 'fixed';
  169. input.style.opacity = 0;
  170. input.value = text;
  171. document.body.appendChild(input);
  172. input.select();
  173. document.execCommand('Copy');
  174. document.body.removeChild(input);
  175. toast('Json片段复制成功,随处粘贴可用!')
  176. };
  177. /**
  178. * 从el中获取json文本
  179. * @param el
  180. * @returns {string}
  181. */
  182. let getJsonText = function (el) {
  183. let txt = el.text().replace(/复制\|下载\|删除/gm,'').replace(/":\s/gm, '":').replace(/,$/, '').trim();
  184. if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
  185. txt = '{' + txt + '}';
  186. }
  187. try {
  188. txt = JSON.stringify(JSON.parse(txt), null, 4);
  189. } catch (err) {
  190. }
  191. return txt;
  192. };
  193. // 添加json路径
  194. let _showJsonPath = function (curEl) {
  195. let keys = [];
  196. do {
  197. if (curEl.hasClass('item-block')) {
  198. if (!curEl.hasClass('rootItem')) {
  199. keys.unshift('[' + curEl.prevAll('.item').length + ']');
  200. } else {
  201. break;
  202. }
  203. } else {
  204. keys.unshift(curEl.find('>.key').text());
  205. }
  206. if (curEl.parent().hasClass('rootItem') || curEl.parent().parent().hasClass('rootItem')) {
  207. break;
  208. }
  209. curEl = curEl.parent().parent();
  210. } while (curEl.length && !curEl.hasClass('rootItem'));
  211. // 过滤掉空值和无效的key,避免产生多余的点号
  212. let validKeys = keys.filter(key => key && key.trim() !== '');
  213. // 构建路径:正确处理对象属性和数组索引的连接
  214. let path = '';
  215. for (let i = 0; i < validKeys.length; i++) {
  216. let key = validKeys[i];
  217. if (key.startsWith('[') && key.endsWith(']')) {
  218. // 数组索引,直接拼接(前面永远不需要点号)
  219. path += key;
  220. } else {
  221. // 对象属性
  222. if (i > 0) {
  223. // 对象属性前面需要点号(数组索引后面的属性也需要点号)
  224. path += '.';
  225. }
  226. path += key;
  227. }
  228. }
  229. let jfPath = $('#jsonPath');
  230. if (!jfPath.length) {
  231. jfPath = $('<span id="jsonPath"/>').prependTo(jfStatusBar);
  232. }
  233. jfPath.html('当前节点:$.' + path);
  234. };
  235. // 给某个节点增加操作项
  236. let _addOptForItem = function (el, show) {
  237. // 下载json片段
  238. let fnDownload = function (event) {
  239. event.stopPropagation();
  240. let txt = getJsonText(el);
  241. // 下载片段
  242. let dt = (new Date()).format('yyyyMMddHHmmss');
  243. let blob = new Blob([txt], {type: 'application/octet-stream'});
  244. if (typeof chrome === 'undefined' || !chrome.permissions) {
  245. // 下载JSON的简单形式
  246. $(this).attr('download', 'FeHelper-' + dt + '.json').attr('href', URL.createObjectURL(blob));
  247. } else {
  248. // 请求权限
  249. chrome.permissions.request({
  250. permissions: ['downloads']
  251. }, (granted) => {
  252. if (granted) {
  253. chrome.downloads.download({
  254. url: URL.createObjectURL(blob),
  255. saveAs: true,
  256. conflictAction: 'overwrite',
  257. filename: 'FeHelper-' + dt + '.json'
  258. });
  259. } else {
  260. toast('必须接受授权,才能正常下载!');
  261. }
  262. });
  263. }
  264. };
  265. // 复制json片段
  266. let fnCopy = function (event) {
  267. event.stopPropagation();
  268. _copyToClipboard(getJsonText(el));
  269. };
  270. // 删除json片段
  271. let fnDel = function (event) {
  272. event.stopPropagation();
  273. if (el.parent().is('#formattedJson')) {
  274. toast('如果连最外层的Json也删掉的话,就没啥意义了哦!');
  275. return false;
  276. }
  277. toast('节点已删除成功!');
  278. el.remove();
  279. jfStatusBar && jfStatusBar.hide();
  280. };
  281. $('.boxOpt').hide();
  282. if (show) {
  283. let jfOptEl = el.children('.boxOpt');
  284. if (!jfOptEl.length) {
  285. jfOptEl = $('<b class="boxOpt">' +
  286. '<a class="opt-copy" title="复制当前选中节点的JSON数据">复制</a>|' +
  287. '<a class="opt-download" target="_blank" title="下载当前选中节点的JSON数据">下载</a>|' +
  288. '<a class="opt-del" title="删除当前选中节点的JSON数据">删除</a></b>').appendTo(el);
  289. } else {
  290. jfOptEl.show();
  291. }
  292. jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
  293. jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
  294. jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
  295. }
  296. };
  297. // 显示当前节点的Key
  298. let _toogleStatusBar = function (curEl, show) {
  299. if (!jfStatusBar) {
  300. jfStatusBar = $('<div id="statusBar"/>').appendTo('body');
  301. }
  302. if (!show) {
  303. jfStatusBar.hide();
  304. return;
  305. } else {
  306. jfStatusBar.show();
  307. }
  308. _showJsonPath(curEl);
  309. };
  310. /**
  311. * 折叠所有
  312. * @param elements
  313. */
  314. function collapse(elements) {
  315. let el;
  316. $.each(elements, function (i) {
  317. el = $(this);
  318. if (el.children('.kv-list').length) {
  319. el.addClass('collapsed');
  320. if (!el.attr('id')) {
  321. el.attr('id', 'item' + (++lastItemIdGiven));
  322. let count = el.children('.kv-list').eq(0).children().length;
  323. // Generate comment text eg "4 items"
  324. let comment = count + (count === 1 ? ' item' : ' items');
  325. // Add CSS that targets it
  326. jfStyleEl[0].insertAdjacentHTML(
  327. 'beforeend',
  328. '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
  329. );
  330. }
  331. }
  332. });
  333. }
  334. /**
  335. * 创建几个全局操作的按钮,置于页面右上角即可
  336. * @private
  337. */
  338. let _buildOptionBar = function () {
  339. let optionBar = $('#optionBar');
  340. if (optionBar.length) {
  341. optionBar.html('');
  342. } else {
  343. optionBar = $('<span id="optionBar" />').appendTo(jfContent.parent());
  344. }
  345. $('<span class="x-split">|</span>').appendTo(optionBar);
  346. let buttonFormatted = $('<button class="xjf-btn xjf-btn-left">元数据</button>').appendTo(optionBar);
  347. let buttonCollapseAll = $('<button class="xjf-btn xjf-btn-mid">折叠所有</button>').appendTo(optionBar);
  348. let plainOn = false;
  349. buttonFormatted.bind('click', function (e) {
  350. if (plainOn) {
  351. plainOn = false;
  352. jfPre.hide();
  353. jfContent.show();
  354. buttonFormatted.text('元数据');
  355. } else {
  356. plainOn = true;
  357. jfPre.show();
  358. jfContent.hide();
  359. buttonFormatted.text('格式化');
  360. }
  361. jfStatusBar && jfStatusBar.hide();
  362. });
  363. buttonCollapseAll.bind('click', function (e) {
  364. // 如果内容还没有格式化过,需要再格式化一下
  365. if (plainOn) {
  366. buttonFormatted.trigger('click');
  367. }
  368. if (buttonCollapseAll.text() === '折叠所有') {
  369. buttonCollapseAll.text('展开所有');
  370. collapse($('.item-object,.item-block'));
  371. } else {
  372. buttonCollapseAll.text('折叠所有');
  373. $('.item-object,.item-block').removeClass('collapsed');
  374. }
  375. jfStatusBar && jfStatusBar.hide();
  376. });
  377. };
  378. // 附加操作
  379. let _addEvents = function () {
  380. // 折叠、展开
  381. $('#jfContent span.expand').bind('click', function (ev) {
  382. ev.preventDefault();
  383. ev.stopPropagation();
  384. let parentEl = $(this).parent();
  385. parentEl.toggleClass('collapsed');
  386. if (parentEl.hasClass('collapsed')) {
  387. collapse(parentEl);
  388. }
  389. });
  390. // 点击选中:高亮
  391. $('#jfContent .item').bind('click', function (e) {
  392. let el = $(this);
  393. if (el.hasClass('x-selected')) {
  394. _toogleStatusBar(el, false);
  395. _addOptForItem(el, false);
  396. el.removeClass('x-selected');
  397. e.stopPropagation();
  398. return true;
  399. }
  400. $('.x-selected').removeClass('x-selected');
  401. el.addClass('x-selected');
  402. // 显示底部状态栏
  403. _toogleStatusBar(el, true);
  404. _addOptForItem(el, true);
  405. if (!$(e.target).is('.item .expand')) {
  406. e.stopPropagation();
  407. } else {
  408. $(e.target).parent().trigger('click');
  409. }
  410. // 触发钩子
  411. if (typeof window._OnJsonItemClickByFH === 'function') {
  412. window._OnJsonItemClickByFH(getJsonText(el));
  413. }
  414. });
  415. // 行悬停效果:只高亮当前直接悬停的item,避免嵌套冒泡
  416. let currentHoverElement = null;
  417. $('#jfContent .item').bind('mouseenter', function (e) {
  418. // 只处理视觉效果,不触发任何其他逻辑
  419. // 清除之前的悬停样式
  420. if (currentHoverElement) {
  421. currentHoverElement.removeClass('fh-hover');
  422. }
  423. // 添加当前悬停样式
  424. let el = $(this);
  425. el.addClass('fh-hover');
  426. currentHoverElement = el;
  427. // 严格阻止事件冒泡和默认行为
  428. e.stopPropagation();
  429. e.stopImmediatePropagation();
  430. e.preventDefault();
  431. });
  432. $('#jfContent .item').bind('mouseleave', function (e) {
  433. // 只处理视觉效果,不触发任何其他逻辑
  434. let el = $(this);
  435. el.removeClass('fh-hover');
  436. // 如果当前移除的元素是记录的悬停元素,清空记录
  437. if (currentHoverElement && currentHoverElement[0] === el[0]) {
  438. currentHoverElement = null;
  439. }
  440. // 严格阻止事件冒泡和默认行为
  441. e.stopPropagation();
  442. e.stopImmediatePropagation();
  443. });
  444. // 为整个jfContent区域添加鼠标离开事件,确保彻底清除悬停样式
  445. $('#jfContent').bind('mouseleave', function (e) {
  446. if (currentHoverElement) {
  447. currentHoverElement.removeClass('fh-hover');
  448. currentHoverElement = null;
  449. }
  450. });
  451. };
  452. /**
  453. * 初始化或获取Worker实例(异步,兼容Chrome/Edge/Firefox)
  454. * @returns {Promise<Worker|null>}
  455. */
  456. let _getWorkerInstance = async function() {
  457. if (workerInstance) {
  458. return workerInstance;
  459. }
  460. let workerUrl = chrome.runtime.getURL('json-format/json-worker.js');
  461. // 判断是否为Firefox
  462. const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
  463. try {
  464. if (isFirefox) {
  465. workerInstance = new Worker(workerUrl);
  466. return workerInstance;
  467. } else {
  468. // Chrome/Edge用fetch+Blob方式
  469. const resp = await fetch(workerUrl);
  470. const workerScript = await resp.text();
  471. const blob = new Blob([workerScript], { type: 'application/javascript' });
  472. const blobUrl = URL.createObjectURL(blob);
  473. workerInstance = new Worker(blobUrl);
  474. return workerInstance;
  475. }
  476. } catch (e) {
  477. console.error('创建Worker失败:', e);
  478. workerInstance = null;
  479. return null;
  480. }
  481. };
  482. /**
  483. * 执行代码格式化
  484. * 支持异步worker
  485. */
  486. let format = async function (jsonStr, skin) {
  487. cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
  488. _initElements();
  489. jfPre.html(htmlspecialchars(cachedJsonString));
  490. try {
  491. // 获取Worker实例(异步)
  492. let worker = await _getWorkerInstance();
  493. if (worker) {
  494. // 设置消息处理程序
  495. worker.onmessage = function (evt) {
  496. let msg = evt.data;
  497. switch (msg[0]) {
  498. case 'FORMATTING':
  499. formattingMsg.show();
  500. break;
  501. case 'FORMATTED':
  502. formattingMsg.hide();
  503. jfContent.html(msg[1]);
  504. _buildOptionBar();
  505. // 事件绑定
  506. _addEvents();
  507. // 支持文件下载
  508. _downloadSupport(cachedJsonString);
  509. break;
  510. }
  511. };
  512. // 发送格式化请求
  513. worker.postMessage({
  514. jsonString: jsonStr,
  515. skin: skin
  516. });
  517. } else {
  518. // Worker创建失败,回退到同步方式
  519. formatSync(jsonStr, skin);
  520. }
  521. } catch (e) {
  522. console.error('Worker处理失败:', e);
  523. // 出现任何错误,回退到同步方式
  524. formatSync(jsonStr, skin);
  525. }
  526. };
  527. // 同步的方式格式化
  528. let formatSync = function (jsonStr, skin) {
  529. cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
  530. _initElements();
  531. jfPre.html(htmlspecialchars(cachedJsonString));
  532. // 显示格式化进度
  533. formattingMsg.show();
  534. try {
  535. // 回退方案:使用简单模式直接显示格式化的JSON
  536. let formattedJson = JSON.stringify(JSON.parse(jsonStr), null, 4);
  537. jfContent.html(`<div id="formattedJson"><pre class="rootItem">${htmlspecialchars(formattedJson)}</pre></div>`);
  538. // 隐藏进度提示
  539. formattingMsg.hide();
  540. // 构建操作栏
  541. _buildOptionBar();
  542. // 事件绑定
  543. _addEvents();
  544. // 支持文件下载
  545. _downloadSupport(cachedJsonString);
  546. return;
  547. } catch (e) {
  548. jfContent.html(`<div class="error">JSON格式化失败: ${e.message}</div>`);
  549. // 隐藏进度提示
  550. formattingMsg.hide();
  551. }
  552. };
  553. return {
  554. format: format,
  555. formatSync: formatSync
  556. }
  557. })();