JSON formatter

Format JSON data in a beautiful way.

اعتبارا من 03-05-2016. شاهد أحدث إصدار.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name        JSON formatter
// @namespace   http://gerald.top
// @author      Gerald <[email protected]>
// @icon        http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=80
// @description Format JSON data in a beautiful way.
// @description:zh-CN 更加漂亮地显示JSON数据。
// @version     1.3
// @match       *://*/*
// @match       file:///*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// ==/UserScript==

function safeHTML(html) {
  return String(html).replace(/[<&"]/g, function (key) {
    return {
      '<': '&lt;',
      '&': '&amp;',
      '"': '&quot;',
    }[key];
  });
}

function getSpace(level) {
  if (!level) return;
  var span = document.createElement('span');
  span.className = 'space';
  span.innerHTML = '&nbsp;'.repeat(2 * level);
  return span;
}

function initSpaces(root, level) {
  level = level || 0;
  var space = getSpace(level);
  space && root.insertBefore(space, root.firstChild);
  var children = root.children;
  for (var i = children.length; i --; ) {
    var child = children[i];
    if (child.tagName.toLowerCase() === 'ul') {
      space && root.insertBefore(space.cloneNode(true), child.nextElementSibling);
      [].forEach.call(child.children, function (li) {
        initSpaces(li, level + 1);
      });
    }
  }
}

function join(list) {
  function open() {
    if (!isOpen) {
      html.push('<li>');
      isOpen = true;
    }
  }
  function close() {
    if (isOpen) {
      html.push('</li>');
      isOpen = false;
    }
  }
  var html = [];
  var isOpen = false;
  list.forEach(function (item, i) {
    var next = list[i + 1];
    open();
    item.data && html.push(item.data);
    next && item.separator && html.push(item.separator);
    if (
      !next
      || next.type === KEY
      || item.type !== KEY && (
        item.type === SINGLELINE || next.type === SINGLELINE
      )
    ) close();
  });
  return html.join('');
}

function getHtml(data) {
  var type = typeof data.value;
  var html = '<span class="' + (data.cls || 'value ' + type) + '" ' +
    'data-type="' + safeHTML(data.type || type) + '" ' +
    'data-value="' + safeHTML(data.value) + '">' + safeHTML(data.value) + '</span>';
  if (data.cls === 'key' || !data.cls && type === 'string')
    html = QUOTE + html + QUOTE;
  return html;
}

function render(data) {
  var ret, arr;
  if (Array.isArray(data)) {
    arr = [];
    ret = {
      type: MULTILINE,
      separator: COMMA,
    };
    arr.push(getHtml({value: '[', cls: 'operator'}));
    if (data.length) {
      arr.push(
        '<ul>',
        join(data.map(render)),
        '</ul>'
      );
    } else {
      arr.push(getHtml({value: '', cls: 'separator'}));
      ret.type = SINGLELINE;
    }
    arr.push(getHtml({value: ']', cls: 'operator'}));
    ret.data = arr.join('');
  } else if (data === null) {
    ret = {
      type: SINGLELINE,
      separator: COMMA,
      data: getHtml({value: data, cls: 'value null'}),
    };
  } else if (typeof data == 'object') {
    arr = [];
    ret = {
      type: MULTILINE,
      separator: COMMA,
    };
    arr.push(getHtml({value: '{', cls: 'operator'}));
    var objdata = [];
    for (var key in data) {
      objdata.push({
        type: KEY,
        data: getHtml({value: key, cls: 'key'}),
        separator: getHtml({value: ':', cls: 'separator'}),
      }, render(data[key]));
    }
    if (objdata.length) {
      arr.push(
        '<ul>',
        join(objdata),
        '</ul>'
      );
    } else {
      arr.push(getHtml({value: '', cls: 'separator'}));
      ret.type = SINGLELINE;
    }
    arr.push(getHtml({value: '}', cls: 'operator'}));
    ret.data = arr.join('');
  } else {
    ret = {
      type: SINGLELINE,
      separator: COMMA,
      data: getHtml({value: data}),
    };
  }
  return ret;
}

function formatJSON() {
  if (formatter.formatted) {
    formatter.tips.hide();
    formatter.menu.detach();
    document.body.innerHTML = formatter.raw;
    formatter.formatted = false;
  } else {
    if (!('raw' in formatter)) {
      formatter.raw = document.body.innerHTML;
      formatter.data = JSON.parse(document.body.innerText);
      formatter.style = GM_addStyle([
        '*{font-family:Microsoft YaHei,Tahoma;font-size:14px;}',
        'body,ul{margin:0;padding:0;}',
        '#root{position:relative;margin:0;padding:1rem;}',
        '#root>ul ul{padding-left:2rem;}',
        '.quote,.comma,.separator{color:#999;}',
        '.space,.hide-quotes .quote,.hide-separators .comma{font-size:0;}',
        'li{list-style:none;}',
        '.comma,.separator{margin-right:.5rem;}',
        '.number{color:darkorange;}',
        '.null{color:gray;}',
        '.key{color:brown;}',
        '.string{color:green;}',
        '.boolean{color:dodgerblue;}',
        '.operator{color:blue;}',
        '.value{cursor:pointer;}',
        '.tips{position:absolute;padding:.5em;border-radius:.5em;box-shadow:0 0 1em gray;background:white;z-index:1;white-space:nowrap;color:black;}',
        '.tips-key{font-weight:bold;}',
        '.tips-val{color:dodgerblue;}',
        '.menu{position:fixed;top:0;right:0;background:white;padding:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}',
        '.menu>span{margin-right:5px;}',
        '.menu .btn{display:inline-block;width:18px;height:18px;line-height:18px;text-align:center;background:#ddd;border-radius:4px;cursor:pointer;}',
        '.menu .btn.active{color:white;background:#444;}',
        '.hide{display:none;}',
      ].join(''));
      initTips();
      initMenu();
      formatter.render = function () {
        var root = formatter.root.querySelector('li');
        root.innerHTML = render(formatter.data).data;
        formatter.update();
        initSpaces(root);
      };
      formatter.update = function () {
        var ul = formatter.root.querySelector('ul');
        formatter.options.forEach(function (item) {
          ul.classList[config[item.key] ? 'add' : 'remove'](item.key);
        });
      };
    }
    document.body.innerHTML = '<div id="root"><ul><li></li></ul></div>';
    formatter.formatted = true;
    formatter.root = document.querySelector('#root');
    formatter.menu.attach();
    bindEvents();
    formatter.render();
  }
}

function removeEl(el) {
  el && el.parentNode && el.parentNode.removeChild(el);
}

function initMenu() {
  var menu = document.createElement('div');
  menu.className = 'menu';
  formatter.options.forEach(function (item) {
    var span = document.createElement('span');
    span.className = 'btn';
    if (config[item.key]) span.className += ' active';
    span.dataset.key = item.key;
    span.innerHTML = item.title;
    menu.appendChild(span);
  });
  menu.addEventListener('click', function (e) {
    var el = e.target;
    var key = el.dataset.key;
    if (key) {
      config[key] = !config[key];
      GM_setValue('config', config);
      el.classList.toggle('active');
      formatter.update();
    }
  }, false);
  formatter.menu = {
    node: menu,
    attach: function () {
      formatter.root.appendChild(menu);
    },
    detach: function () {
      removeEl(menu);
    },
  };
}

function initTips() {
  function hide() {
    removeEl(tips);
  }
  var tips = document.createElement('div');
  tips.className = 'tips';
  tips.addEventListener('click', function (e) {
    e.stopPropagation();
  }, false);
  document.addEventListener('click', hide, false);
  formatter.tips = {
    node: tips,
    hide: hide,
    show: function (range) {
      var gap = 5;
      var scrollTop = document.body.scrollTop;
      var rects = range.getClientRects(), rect;
      if (rects[0].top < 100) {
        rect = rects[rects.length - 1];
        tips.style.top = rect.bottom + scrollTop + gap + 'px';
        tips.style.bottom = '';
      } else {
        rect = rects[0];
        tips.style.top = '';
        tips.style.bottom = formatter.root.offsetHeight - rect.top - scrollTop + gap + 'px';
      }
      tips.style.left = rect.left + 'px';
      tips.innerHTML = '<span class="tips-key">type</span>: <span class="tips-val">' + safeHTML(range.startContainer.dataset.type) + '</span>';
      formatter.root.appendChild(tips);
    },
  };
}

function selectNode(node) {
  var selection = window.getSelection();
  selection.removeAllRanges();
  var range = document.createRange();
  range.setStartBefore(node.firstChild);
  range.setEndAfter(node.firstChild);
  selection.addRange(range);
  return range;
}

function bindEvents() {
  formatter.root.addEventListener('click', function (e) {
    e.stopPropagation();
    var target = e.target;
    if (target.classList.contains('value')) {
      formatter.tips.show(selectNode(target));
    } else {
      formatter.tips.hide();
    }
  }, false);
}

var getId = function () {
  var id = 0;
  return function () {
    return ++ id;
  };
}();
var SINGLELINE = getId();
var MULTILINE = getId();
var KEY = getId();
var QUOTE = '<span class="quote">"</span>';
var COMMA = '<span class="comma">,</span>';

var formatter = {
  options: [{
    key: 'hide-quotes',
    title: '"',
    def: false,
  }, {
    key: 'hide-separators',
    title: ',',
    def: false,
  }],
};
var config = GM_getValue('config', formatter.options.reduce(function (res, item) {
  res[item.key] = item.def;
  return res;
}, {}));

~[
  'application/json',
  'text/plain',
].indexOf(document.contentType) && formatJSON();
GM_registerMenuCommand('Toggle JSON format', formatJSON);