format-lib.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  1. /**
  2. * FeHelper Json Format Lib
  3. */
  4. var JsonFormatEntrance = (function () {
  5. "use strict";
  6. var jfContent,
  7. pre,
  8. jfStyleEl,
  9. formattingMsg,
  10. slowAnalysisTimeout,
  11. isJsonTime,
  12. exitedNotJsonTime,
  13. displayedFormattedJsonTime
  14. ;
  15. // Add listener to receive response from BG when ready
  16. var postMessage = function (msg) {
  17. // console.log('Port msg received', msg[0], (""+msg[1]).substring(0,30)) ;
  18. switch (msg[0]) {
  19. case 'NOT JSON' :
  20. pre.style.display = "";
  21. // console.log('Unhidden the PRE') ;
  22. jfContent.innerHTML = '<span class="x-json-tips">JSON不合法,请检查:</span>';
  23. exitedNotJsonTime = +(new Date());
  24. break;
  25. case 'FORMATTING' :
  26. isJsonTime = +(new Date());
  27. // It is JSON, and it's now being formatted in the background worker.
  28. // Clear the slowAnalysisTimeout (if the BG worker had taken longer than 1s to respond with an answer to whether or not this is JSON, then it would have fired, unhiding the PRE... But now that we know it's JSON, we can clear this timeout, ensuring the PRE stays hidden.)
  29. clearTimeout(slowAnalysisTimeout);
  30. // Create option bar
  31. var optionBar = document.getElementById('optionBar');
  32. if (optionBar) {
  33. optionBar.parentNode.removeChild(optionBar);
  34. }
  35. optionBar = document.createElement('div');
  36. optionBar.id = 'optionBar';
  37. // Create toggleFormat button
  38. var buttonFormatted = document.createElement('button'),
  39. buttonCollapseAll = document.createElement('button');
  40. buttonFormatted.id = 'buttonFormatted';
  41. buttonFormatted.innerText = '元数据';
  42. buttonFormatted.classList.add('selected');
  43. buttonCollapseAll.id = 'buttonCollapseAll';
  44. buttonCollapseAll.innerText = '折叠所有';
  45. var plainOn = false;
  46. buttonFormatted.addEventListener('click', function () {
  47. // When formatted button clicked...
  48. if (plainOn) {
  49. plainOn = false;
  50. pre.style.display = "none";
  51. jfContent.style.display = "block";
  52. $(this).text('元数据');
  53. } else {
  54. plainOn = true;
  55. pre.style.display = "block";
  56. jfContent.style.display = "none";
  57. $(this).text('格式化');
  58. }
  59. $(this).parent().find('button').removeClass('selected');
  60. $(this).addClass('selected');
  61. $('#boxOpt').hide();
  62. }, false);
  63. buttonCollapseAll.addEventListener('click', function () {
  64. // 如果内容还没有格式化过,需要再格式化一下
  65. if (plainOn) {
  66. buttonFormatted.click();
  67. }
  68. // When collapaseAll button clicked...
  69. if (!plainOn) {
  70. if (buttonCollapseAll.innerText === '折叠所有') {
  71. buttonCollapseAll.innerText = '展开所有';
  72. collapse(document.getElementsByClassName('objProp'));
  73. } else {
  74. buttonCollapseAll.innerText = '折叠所有';
  75. expand(document.getElementsByClassName('objProp'));
  76. }
  77. $(this).parent().find('button').removeClass('selected');
  78. $(this).addClass('selected');
  79. }
  80. $('#boxOpt').hide();
  81. }, false);
  82. // Put it in optionBar
  83. optionBar.appendChild(buttonFormatted);
  84. optionBar.appendChild(buttonCollapseAll);
  85. // Attach event handlers
  86. document.addEventListener('click', generalClick, false);
  87. // Put option bar in DOM
  88. jfContent.parentNode.appendChild(optionBar);
  89. break;
  90. case 'FORMATTED' :
  91. // Insert HTML content
  92. formattingMsg.style.display = "";
  93. jfContent.innerHTML = msg[1];
  94. displayedFormattedJsonTime = +(new Date());
  95. break;
  96. default :
  97. throw new Error('Message not understood: ' + msg[0]);
  98. }
  99. };
  100. // console.timeEnd('established port') ;
  101. var lastKvovIdGiven = 0;
  102. function collapse(elements) {
  103. var el, i, blockInner, count;
  104. for (i = elements.length - 1; i >= 0; i--) {
  105. el = elements[i];
  106. el.classList.add('collapsed');
  107. // (CSS hides the contents and shows an ellipsis.)
  108. // Add a count of the number of child properties/items (if not already done for this item)
  109. if (!el.id) {
  110. el.id = 'kvov' + (++lastKvovIdGiven);
  111. // Find the blockInner
  112. blockInner = el.firstElementChild;
  113. while (blockInner && !blockInner.classList.contains('blockInner')) {
  114. blockInner = blockInner.nextElementSibling;
  115. }
  116. if (!blockInner)
  117. continue;
  118. // See how many children in the blockInner
  119. count = blockInner.children.length;
  120. // Generate comment text eg "4 items"
  121. var comment = count + (count === 1 ? ' item' : ' items');
  122. // Add CSS that targets it
  123. jfStyleEl.insertAdjacentHTML(
  124. 'beforeend',
  125. '\n#kvov' + lastKvovIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
  126. );
  127. }
  128. }
  129. }
  130. function expand(elements) {
  131. for (var i = elements.length - 1; i >= 0; i--)
  132. elements[i].classList.remove('collapsed');
  133. }
  134. var mac = navigator.platform.indexOf('Mac') !== -1,
  135. modKey;
  136. if (mac)
  137. modKey = function (ev) {
  138. return ev.metaKey;
  139. };
  140. else
  141. modKey = function (ev) {
  142. return ev.ctrlKey;
  143. };
  144. function generalClick(ev) {
  145. // console.log('click', ev) ;
  146. if (ev.which === 1) {
  147. var elem = ev.target;
  148. if (elem.className === 'e') {
  149. // It's a click on an expander.
  150. ev.preventDefault();
  151. var parent = elem.parentNode,
  152. div = jfContent,
  153. prevBodyHeight = document.body.offsetHeight,
  154. scrollTop = document.body.scrollTop,
  155. parentSiblings
  156. ;
  157. // Expand or collapse
  158. if (parent.classList.contains('collapsed')) {
  159. // EXPAND
  160. if (modKey(ev))
  161. expand(parent.parentNode.children);
  162. else
  163. expand([parent]);
  164. }
  165. else {
  166. // COLLAPSE
  167. if (modKey(ev))
  168. collapse(parent.parentNode.children);
  169. else
  170. collapse([parent]);
  171. }
  172. // Restore scrollTop somehow
  173. // Clear current extra margin, if any
  174. div.style.marginBottom = 0;
  175. // No need to worry if all content fits in viewport
  176. if (document.body.offsetHeight < window.innerHeight) {
  177. // console.log('document.body.offsetHeight < window.innerHeight; no need to adjust height') ;
  178. return;
  179. }
  180. // And no need to worry if scrollTop still the same
  181. if (document.body.scrollTop === scrollTop) {
  182. // console.log('document.body.scrollTop === scrollTop; no need to adjust height') ;
  183. return;
  184. }
  185. // console.log('Scrolltop HAS changed. document.body.scrollTop is now '+document.body.scrollTop+'; was '+scrollTop) ;
  186. // The body has got a bit shorter.
  187. // We need to increase the body height by a bit (by increasing the bottom margin on the jfContent div). The amount to increase it is whatever is the difference between our previous scrollTop and our new one.
  188. // Work out how much more our target scrollTop is than this.
  189. var difference = scrollTop - document.body.scrollTop + 8; // it always loses 8px; don't know why
  190. // Add this difference to the bottom margin
  191. //var currentMarginBottom = parseInt(div.style.marginBottom) || 0 ;
  192. div.style.marginBottom = difference + 'px';
  193. // Now change the scrollTop back to what it was
  194. document.body.scrollTop = scrollTop;
  195. return;
  196. }
  197. }
  198. }
  199. /**
  200. * 执行代码格式化
  201. * @param {[type]} jsonStr [description]
  202. * @return {[type]}
  203. */
  204. var format = function (jsonStr) {
  205. try {
  206. jfContent.innerHTML = '';
  207. pre.innerHTML = '';
  208. document.querySelector('#boxOpt').remove();
  209. } catch (e) {
  210. }
  211. // Send the contents of the PRE to the BG script
  212. // Add jfContent DIV, ready to display stuff
  213. jfContent = document.getElementById('jfContent');
  214. if (!jfContent) {
  215. jfContent = document.createElement('div');
  216. jfContent.id = 'jfContent';
  217. document.body.appendChild(jfContent);
  218. }
  219. jfContent.style.display = '';
  220. pre = document.getElementById('jfContent_pre');
  221. if (!pre) {
  222. pre = document.createElement('pre');
  223. pre.id = 'jfContent_pre';
  224. document.body.appendChild(pre);
  225. }
  226. pre.innerHTML = JSON.stringify(JSON.parse(jsonStr), null, 4);
  227. pre.style.display = "none";
  228. jfStyleEl = document.getElementById('jfStyleEl');
  229. if (!jfStyleEl) {
  230. jfStyleEl = document.createElement('style');
  231. document.head.appendChild(jfStyleEl);
  232. }
  233. formattingMsg = document.getElementById('formattingMsg');
  234. if (!formattingMsg) {
  235. formattingMsg = document.createElement('pre');
  236. formattingMsg.id = 'formattingMsg';
  237. formattingMsg.innerHTML = '<svg id="spinner" width="16" height="16" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" version="1.1">' +
  238. '<path d="M 150,0 a 150,150 0 0,1 106.066,256.066 l -35.355,-35.355 a -100,-100 0 0,0 -70.711,-170.711 z" fill="#3d7fe6"></path></svg> 格式化中...';
  239. document.body.appendChild(formattingMsg);
  240. }
  241. // Post the contents of the PRE
  242. JsonFormatDealer.postMessage({
  243. type: "SENDING TEXT",
  244. text: jsonStr,
  245. length: jsonStr.length
  246. });
  247. _loadJs();
  248. // 事件绑定
  249. _addEvents();
  250. // 支持文件下载
  251. _downloadSupport(JSON.stringify(JSON.parse(jsonStr), null, 4));
  252. };
  253. var _loadJs = function () {
  254. if (typeof Tarp === 'object') {
  255. Tarp.require('../static/js/utils.js');
  256. } else {
  257. alert('无法加载Tarp.require.js');
  258. }
  259. };
  260. /**
  261. * 直接下载,能解决中文乱码
  262. * @param content
  263. * @private
  264. */
  265. var _downloadSupport = function (content) {
  266. // 下载链接
  267. var localUrl = location.href;
  268. var dt = (new Date()).format('yyyyMMddHHmmss');
  269. content = ['/* ', localUrl, ' */', '\n', content].join('');
  270. var blob = new Blob([content], {type: 'application/octet-stream'});
  271. var button = $('<button id="btnDownload">下载JSON</button>').appendTo('#optionBar');
  272. if (typeof chrome === 'undefined' || !chrome.permissions) {
  273. button.click(function (e) {
  274. var aLink = $('<a id="btnDownload" target="_blank" title="保存到本地">下载JSON数据</a>');
  275. aLink.attr('download', 'FeHelper-' + dt + '.json');
  276. aLink.attr('href', URL.createObjectURL(blob));
  277. aLink[0].click();
  278. });
  279. } else {
  280. button.click(function (e) {
  281. // 请求权限
  282. chrome.permissions.request({
  283. permissions: ['downloads']
  284. }, (granted) => {
  285. if (granted) {
  286. chrome.downloads.download({
  287. url: URL.createObjectURL(blob),
  288. saveAs: true,
  289. conflictAction: 'overwrite',
  290. filename: 'FeHelper-' + dt + '.json'
  291. });
  292. } else {
  293. alert('必须接受授权,才能正常下载!');
  294. }
  295. });
  296. });
  297. }
  298. };
  299. /**
  300. * chrome 下复制到剪贴板
  301. * @param text
  302. */
  303. var _copyToClipboard = function (text) {
  304. var input = document.createElement('textarea');
  305. input.style.position = 'fixed';
  306. input.style.opacity = 0;
  307. input.value = text;
  308. document.body.appendChild(input);
  309. input.select();
  310. document.execCommand('Copy');
  311. document.body.removeChild(input);
  312. alert('Json片段复制成功,随处粘贴可用!')
  313. };
  314. /**
  315. * 从el中获取json文本
  316. * @param el
  317. * @returns {string}
  318. */
  319. var getJsonText = function (el) {
  320. var txt = el.text().replace(/":\s/gm, '":').replace(/,$/, '').trim();
  321. if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
  322. txt = '{' + txt + '}';
  323. }
  324. try {
  325. txt = JSON.stringify(JSON.parse(txt), null, 4);
  326. } catch (err) {
  327. }
  328. return txt;
  329. };
  330. /**
  331. * 给某个节点增加操作项
  332. * @param el
  333. * @private
  334. */
  335. var _addOptForItem = function (el) {
  336. // 下载json片段
  337. var fnDownload = function (ec) {
  338. var txt = getJsonText(el);
  339. // 下载片段
  340. var dt = (new Date()).format('yyyyMMddHHmmss');
  341. var blob = new Blob([txt], {type: 'application/octet-stream'});
  342. if (typeof chrome === 'undefined' || !chrome.permissions) {
  343. // 下载JSON的简单形式
  344. $(this).attr('download', 'FeHelper-' + dt + '.json').attr('href', URL.createObjectURL(blob));
  345. } else {
  346. // 请求权限
  347. chrome.permissions.request({
  348. permissions: ['downloads']
  349. }, (granted) => {
  350. if (granted) {
  351. chrome.downloads.download({
  352. url: URL.createObjectURL(blob),
  353. saveAs: true,
  354. conflictAction: 'overwrite',
  355. filename: 'FeHelper-' + dt + '.json'
  356. });
  357. } else {
  358. alert('必须接受授权,才能正常下载!');
  359. }
  360. });
  361. }
  362. };
  363. // 复制json片段
  364. var fnCopy = function (ec) {
  365. _copyToClipboard(getJsonText(el));
  366. };
  367. // 删除json片段
  368. var fnDel = function (ed) {
  369. if (el.parent().is('#formattedJson')) {
  370. alert('如果连最外层的Json也删掉的话,就没啥意义了哦!');
  371. return false;
  372. }
  373. alert('节点已删除成功!');
  374. el.remove();
  375. boxOpt.css('top', -1000).hide();
  376. };
  377. var boxOpt = $('#boxOpt');
  378. if (!boxOpt.length) {
  379. boxOpt = $('<div id="boxOpt"><a class="opt-download" target="_blank">下载</a>|<a class="opt-copy">复制</a>|<a class="opt-del">删除</a></div>').appendTo('body');
  380. }
  381. boxOpt.find('a.opt-download').unbind('click').bind('click', fnDownload);
  382. boxOpt.find('a.opt-copy').unbind('click').bind('click', fnCopy);
  383. boxOpt.find('a.opt-del').unbind('click').bind('click', fnDel);
  384. boxOpt.css({
  385. left: el.offset().left + el.width() - 90,
  386. top: el.offset().top
  387. }).show();
  388. };
  389. // 附加操作
  390. var _addEvents = function () {
  391. $('#jfContent .kvov').bind('click', function (e) {
  392. if ($(this).hasClass('x-outline')) {
  393. $('#boxOpt').remove();
  394. $(this).removeClass('x-outline');
  395. return false;
  396. }
  397. $('.x-outline').removeClass('x-outline');
  398. var el = $(this).removeClass('x-hover').addClass('x-outline');
  399. // 增加复制、删除功能
  400. _addOptForItem(el);
  401. if (!$(e.target).is('.kvov .e')) {
  402. e.stopPropagation();
  403. } else {
  404. $(e.target).parent().trigger('click');
  405. }
  406. // 触发钩子
  407. if (typeof window._OnJsonItemClickByFH === 'function') {
  408. window._OnJsonItemClickByFH(getJsonText(el));
  409. }
  410. }).bind('mouseover', function (e) {
  411. $(this).addClass('x-hover');
  412. return false;
  413. }).bind('mouseout', function (e) {
  414. $(this).removeClass('x-hover');
  415. });
  416. };
  417. return {
  418. format: format,
  419. postMessage: postMessage
  420. }
  421. })();
  422. var JsonFormatDealer = (function () {
  423. "use strict";
  424. // Constants
  425. var
  426. TYPE_STRING = 1,
  427. TYPE_NUMBER = 2,
  428. TYPE_OBJECT = 3,
  429. TYPE_ARRAY = 4,
  430. TYPE_BOOL = 5,
  431. TYPE_NULL = 6
  432. ;
  433. // Utility functions
  434. function removeComments(str) {
  435. str = ('__' + str + '__').split('');
  436. var mode = {
  437. singleQuote: false,
  438. doubleQuote: false,
  439. regex: false,
  440. blockComment: false,
  441. lineComment: false,
  442. condComp: false
  443. };
  444. for (var i = 0, l = str.length; i < l; i++) {
  445. if (mode.regex) {
  446. if (str[i] === '/' && str[i - 1] !== '\\') {
  447. mode.regex = false;
  448. }
  449. continue;
  450. }
  451. if (mode.singleQuote) {
  452. if (str[i] === "'" && str[i - 1] !== '\\') {
  453. mode.singleQuote = false;
  454. }
  455. continue;
  456. }
  457. if (mode.doubleQuote) {
  458. if (str[i] === '"' && str[i - 1] !== '\\') {
  459. mode.doubleQuote = false;
  460. }
  461. continue;
  462. }
  463. if (mode.blockComment) {
  464. if (str[i] === '*' && str[i + 1] === '/') {
  465. str[i + 1] = '';
  466. mode.blockComment = false;
  467. }
  468. str[i] = '';
  469. continue;
  470. }
  471. if (mode.lineComment) {
  472. if (str[i + 1] === '\n' || str[i + 1] === '\r') {
  473. mode.lineComment = false;
  474. }
  475. str[i] = '';
  476. continue;
  477. }
  478. if (mode.condComp) {
  479. if (str[i - 2] === '@' && str[i - 1] === '*' && str[i] === '/') {
  480. mode.condComp = false;
  481. }
  482. continue;
  483. }
  484. mode.doubleQuote = str[i] === '"';
  485. mode.singleQuote = str[i] === "'";
  486. if (str[i] === '/') {
  487. if (str[i + 1] === '*' && str[i + 2] === '@') {
  488. mode.condComp = true;
  489. continue;
  490. }
  491. if (str[i + 1] === '*') {
  492. str[i] = '';
  493. mode.blockComment = true;
  494. continue;
  495. }
  496. if (str[i + 1] === '/') {
  497. str[i] = '';
  498. mode.lineComment = true;
  499. continue;
  500. }
  501. mode.regex = true;
  502. }
  503. }
  504. return str.join('').slice(2, -2);
  505. }
  506. // function spin(seconds) {
  507. // // spin - Hog the CPU for the specified number of seconds
  508. // // (for simulating long processing times in development)
  509. // var stop = +new Date() + (seconds*1000) ;
  510. // while (new Date() < stop) {}
  511. // return true ;
  512. // }
  513. // Record current version (in case future update wants to know)
  514. localStorage.jfVersion = '0.5.6';
  515. // Template elements
  516. var templates,
  517. baseDiv = document.createElement('div'),
  518. baseSpan = document.createElement('span');
  519. function getSpanBoth(innerText, className) {
  520. var span = baseSpan.cloneNode(false);
  521. span.className = className;
  522. span.innerText = innerText;
  523. return span;
  524. }
  525. function getSpanText(innerText) {
  526. var span = baseSpan.cloneNode(false);
  527. span.innerText = innerText;
  528. return span;
  529. }
  530. function getSpanClass(className) {
  531. var span = baseSpan.cloneNode(false);
  532. span.className = className;
  533. return span;
  534. }
  535. function getDivClass(className) {
  536. var span = baseDiv.cloneNode(false);
  537. span.className = className;
  538. return span;
  539. }
  540. // Create template nodes
  541. var templatesObj = {
  542. t_kvov: getDivClass('kvov'),
  543. t_exp: getSpanClass('e'),
  544. t_key: getSpanClass('k'),
  545. t_string: getSpanClass('s'),
  546. t_number: getSpanClass('n'),
  547. t_null: getSpanBoth('null', 'nl'),
  548. t_true: getSpanBoth('true', 'bl'),
  549. t_false: getSpanBoth('false', 'bl'),
  550. t_oBrace: getSpanBoth('{', 'b'),
  551. t_cBrace: getSpanBoth('}', 'b'),
  552. t_oBracket: getSpanBoth('[', 'b'),
  553. t_cBracket: getSpanBoth(']', 'b'),
  554. t_ellipsis: getSpanClass('ell'),
  555. t_blockInner: getSpanClass('blockInner'),
  556. t_colonAndSpace: document.createTextNode(':\u00A0'),
  557. t_commaText: document.createTextNode(','),
  558. t_dblqText: document.createTextNode('"')
  559. };
  560. // Core recursive DOM-building function
  561. function getKvovDOM(value, keyName) {
  562. var type,
  563. kvov,
  564. nonZeroSize,
  565. templates = templatesObj, // bring into scope for tiny speed boost
  566. objKey,
  567. keySpan,
  568. valueElement
  569. ;
  570. // Establish value type
  571. if (typeof value === 'string')
  572. type = TYPE_STRING;
  573. else if (typeof value === 'number')
  574. type = TYPE_NUMBER;
  575. else if (value === false || value === true)
  576. type = TYPE_BOOL;
  577. else if (value === null)
  578. type = TYPE_NULL;
  579. else if (value instanceof Array)
  580. type = TYPE_ARRAY;
  581. else
  582. type = TYPE_OBJECT;
  583. // Root node for this kvov
  584. kvov = templates.t_kvov.cloneNode(false);
  585. // Add an 'expander' first (if this is object/array with non-zero size)
  586. if (type === TYPE_OBJECT || type === TYPE_ARRAY) {
  587. if (typeof JSON.BigNumber === 'function' && value instanceof JSON.BigNumber) {
  588. value = JSON.stringify(value);
  589. type = TYPE_NUMBER;
  590. } else {
  591. nonZeroSize = false;
  592. for (objKey in value) {
  593. if (value.hasOwnProperty(objKey)) {
  594. nonZeroSize = true;
  595. break; // no need to keep counting; only need one
  596. }
  597. }
  598. if (nonZeroSize)
  599. kvov.appendChild(templates.t_exp.cloneNode(false));
  600. }
  601. }
  602. // If there's a key, add that before the value
  603. if (keyName !== false) { // NB: "" is a legal keyname in JSON
  604. // This kvov must be an object property
  605. kvov.classList.add('objProp');
  606. // Create a span for the key name
  607. keySpan = templates.t_key.cloneNode(false);
  608. keySpan.textContent = JSON.stringify(keyName).slice(1, -1); // remove quotes
  609. // Add it to kvov, with quote marks
  610. kvov.appendChild(templates.t_dblqText.cloneNode(false));
  611. kvov.appendChild(keySpan);
  612. kvov.appendChild(templates.t_dblqText.cloneNode(false));
  613. // Also add ":&nbsp;" (colon and non-breaking space)
  614. kvov.appendChild(templates.t_colonAndSpace.cloneNode(false));
  615. }
  616. else {
  617. // This is an array element instead
  618. kvov.classList.add('arrElem');
  619. }
  620. // Generate DOM for this value
  621. var blockInner, childKvov;
  622. switch (type) {
  623. case TYPE_STRING:
  624. // If string is a URL, get a link, otherwise get a span
  625. var innerStringEl = baseSpan.cloneNode(false),
  626. escapedString = JSON.stringify(value);
  627. escapedString = escapedString.substring(1, escapedString.length - 1); // remove quotes
  628. if (value[0] === 'h' && value.substring(0, 4) === 'http') { // crude but fast - some false positives, but rare, and UX doesn't suffer terribly from them.
  629. var innerStringA = document.createElement('A');
  630. innerStringA.href = value;
  631. innerStringA.innerText = escapedString;
  632. innerStringEl.appendChild(innerStringA);
  633. }
  634. else {
  635. innerStringEl.innerText = escapedString;
  636. }
  637. valueElement = templates.t_string.cloneNode(false);
  638. valueElement.appendChild(templates.t_dblqText.cloneNode(false));
  639. valueElement.appendChild(innerStringEl);
  640. valueElement.appendChild(templates.t_dblqText.cloneNode(false));
  641. kvov.appendChild(valueElement);
  642. break;
  643. case TYPE_NUMBER:
  644. // Simply add a number element (span.n)
  645. valueElement = templates.t_number.cloneNode(false);
  646. valueElement.innerText = value;
  647. kvov.appendChild(valueElement);
  648. break;
  649. case TYPE_OBJECT:
  650. // Add opening brace
  651. kvov.appendChild(templates.t_oBrace.cloneNode(true));
  652. // If any properties, add a blockInner containing k/v pair(s)
  653. if (nonZeroSize) {
  654. // Add ellipsis (empty, but will be made to do something when kvov is collapsed)
  655. kvov.appendChild(templates.t_ellipsis.cloneNode(false));
  656. // Create blockInner, which indents (don't attach yet)
  657. blockInner = templates.t_blockInner.cloneNode(false);
  658. // For each key/value pair, add as a kvov to blockInner
  659. var count = 0, k, comma;
  660. for (k in value) {
  661. if (value.hasOwnProperty(k)) {
  662. count++;
  663. childKvov = getKvovDOM(value[k], k);
  664. // Add comma
  665. comma = templates.t_commaText.cloneNode();
  666. childKvov.appendChild(comma);
  667. blockInner.appendChild(childKvov);
  668. }
  669. }
  670. // Now remove the last comma
  671. childKvov.removeChild(comma);
  672. // Add blockInner
  673. kvov.appendChild(blockInner);
  674. }
  675. // Add closing brace
  676. kvov.appendChild(templates.t_cBrace.cloneNode(true));
  677. break;
  678. case TYPE_ARRAY:
  679. // Add opening bracket
  680. kvov.appendChild(templates.t_oBracket.cloneNode(true));
  681. // If non-zero length array, add blockInner containing inner vals
  682. if (nonZeroSize) {
  683. // Add ellipsis
  684. kvov.appendChild(templates.t_ellipsis.cloneNode(false));
  685. // Create blockInner (which indents) (don't attach yet)
  686. blockInner = templates.t_blockInner.cloneNode(false);
  687. // For each key/value pair, add the markup
  688. for (var i = 0, length = value.length, lastIndex = length - 1; i < length; i++) {
  689. // Make a new kvov, with no key
  690. childKvov = getKvovDOM(value[i], false);
  691. // Add comma if not last one
  692. if (i < lastIndex)
  693. childKvov.appendChild(templates.t_commaText.cloneNode());
  694. // Append the child kvov
  695. blockInner.appendChild(childKvov);
  696. }
  697. // Add blockInner
  698. kvov.appendChild(blockInner);
  699. }
  700. // Add closing bracket
  701. kvov.appendChild(templates.t_cBracket.cloneNode(true));
  702. break;
  703. case TYPE_BOOL:
  704. if (value)
  705. kvov.appendChild(templates.t_true.cloneNode(true));
  706. else
  707. kvov.appendChild(templates.t_false.cloneNode(true));
  708. break;
  709. case TYPE_NULL:
  710. kvov.appendChild(templates.t_null.cloneNode(true));
  711. break;
  712. }
  713. return kvov;
  714. }
  715. // Function to convert object to an HTML string
  716. function jsonObjToHTML(obj, jsonpFunctionName) {
  717. // spin(5) ;
  718. // Format object (using recursive kvov builder)
  719. var rootKvov = getKvovDOM(obj, false);
  720. // The whole DOM is now built.
  721. // Set class on root node to identify it
  722. rootKvov.classList.add('rootKvov');
  723. // Make div#formattedJson and append the root kvov
  724. var divFormattedJson = document.createElement('DIV');
  725. divFormattedJson.id = 'formattedJson';
  726. divFormattedJson.appendChild(rootKvov);
  727. // Convert it to an HTML string (shame about this step, but necessary for passing it through to the content page)
  728. var returnHTML = divFormattedJson.outerHTML;
  729. // Top and tail with JSONP padding if necessary
  730. if (jsonpFunctionName !== null) {
  731. returnHTML =
  732. '<div id="jsonpOpener">' + jsonpFunctionName + ' ( </div>' +
  733. returnHTML +
  734. '<div id="jsonpCloser">)</div>';
  735. }
  736. // Return the HTML
  737. return returnHTML;
  738. }
  739. // Listen for requests from content pages wanting to set up a port
  740. var postMessage = function (msg) {
  741. var jsonpFunctionName = null;
  742. if (msg.type === 'SENDING TEXT') {
  743. // Try to parse as JSON
  744. var obj,
  745. text = msg.text;
  746. try {
  747. obj = JSON.parse(text);
  748. }
  749. catch (e) {
  750. // Not JSON; could be JSONP though.
  751. // Try stripping 'padding' (if any), and try parsing it again
  752. text = text.trim();
  753. // Find where the first paren is (and exit if none)
  754. var indexOfParen;
  755. if (!(indexOfParen = text.indexOf('('))) {
  756. JsonFormatEntrance.postMessage(['NOT JSON', 'no opening parenthesis']);
  757. return;
  758. }
  759. // Get the substring up to the first "(", with any comments/whitespace stripped out
  760. var firstBit = removeComments(text.substring(0, indexOfParen)).trim();
  761. if (!firstBit.match(/^[a-zA-Z_$][\.\[\]'"0-9a-zA-Z_$]*$/)) {
  762. // The 'firstBit' is NOT a valid function identifier.
  763. JsonFormatEntrance.postMessage(['NOT JSON', 'first bit not a valid function name']);
  764. return;
  765. }
  766. // Find last parenthesis (exit if none)
  767. var indexOfLastParen;
  768. if (!(indexOfLastParen = text.lastIndexOf(')'))) {
  769. JsonFormatEntrance.postMessage(['NOT JSON', 'no closing paren']);
  770. return;
  771. }
  772. // Check that what's after the last parenthesis is just whitespace, comments, and possibly a semicolon (exit if anything else)
  773. var lastBit = removeComments(text.substring(indexOfLastParen + 1)).trim();
  774. if (lastBit !== "" && lastBit !== ';') {
  775. JsonFormatEntrance.postMessage(['NOT JSON', 'last closing paren followed by invalid characters']);
  776. return;
  777. }
  778. // So, it looks like a valid JS function call, but we don't know whether it's JSON inside the parentheses...
  779. // Check if the 'argument' is actually JSON (and record the parsed result)
  780. text = text.substring(indexOfParen + 1, indexOfLastParen);
  781. try {
  782. obj = JSON.parse(text);
  783. }
  784. catch (e2) {
  785. // Just some other text that happens to be in a function call.
  786. // Respond as not JSON, and exit
  787. JsonFormatEntrance.postMessage(['NOT JSON', 'looks like a function call, but the parameter is not valid JSON']);
  788. return;
  789. }
  790. jsonpFunctionName = firstBit;
  791. }
  792. // If still running, we now have obj, which is valid JSON.
  793. // Ensure it's not a number or string (technically valid JSON, but no point prettifying it)
  794. if (typeof obj !== 'object' && typeof obj !== 'array') {
  795. JsonFormatEntrance.postMessage(['NOT JSON', 'technically JSON but not an object or array']);
  796. return;
  797. }
  798. // And send it the message to confirm that we're now formatting (so it can show a spinner)
  799. JsonFormatEntrance.postMessage(['FORMATTING' /*, JSON.stringify(localStorage)*/]);
  800. // Do formatting
  801. var html = jsonObjToHTML(obj, jsonpFunctionName);
  802. // Post the HTML string to the content script
  803. JsonFormatEntrance.postMessage(['FORMATTED', html]);
  804. }
  805. };
  806. return {
  807. postMessage: postMessage
  808. };
  809. })();
  810. module.exports = {
  811. format: JsonFormatEntrance.format
  812. };