JSON formatter

Format JSON data in a beautiful way.

Versión del día 13/11/2017. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

'use strict';

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

// ==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.5.0
// @match       *://*/*
// @match       file:///*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// ==/UserScript==

var id = 0;
var getId = function getId() {
  return id += 1;
};
var SINGLELINE = getId();
var MULTILINE = getId();
var KEY = getId();
var gap = 5;

var createQuote = function createQuote() {
  return createElement('span', {
    className: 'subtle quote',
    textContent: '"'
  });
};
var createComma = function createComma() {
  return createElement('span', {
    className: 'subtle comma',
    textContent: ','
  });
};
var createSpace = function createSpace() {
  var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
  return createElement('span', {
    className: 'space',
    textContent: ' '.repeat(n)
  });
};
var createIndent = function createIndent() {
  var n = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
  return createSpace(2 * n);
};
var createBr = function createBr() {
  return createElement('br');
};

var formatter = {
  options: [{
    key: 'hide-quotes',
    title: '"',
    def: false
  }, {
    key: 'hide-commas',
    title: ',',
    def: false
  }]
};

var config = GM_getValue('config', formatter.options.reduce(function (res, item) {
  res[item.key] = item.def;
  return res;
}, {}));

if (['application/json', 'text/plain', 'application/javascript', 'text/javascript'].includes(document.contentType)) formatJSON();
GM_registerMenuCommand('Toggle JSON format', formatJSON);

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

function createElement(tag, props) {
  var el = document.createElement(tag);
  if (props) {
    Object.keys(props).forEach(function (key) {
      el[key] = props[key];
    });
  }
  return el;
}

function join(rendered) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

  var arr = [];
  for (var i = 0; i < rendered.length; i += 1) {
    var item = rendered[i];
    var next = rendered[i + 1];
    if (item.data) arr.push.apply(arr, _toConsumableArray(item.data));
    if (next) {
      if (item.separator) arr.push.apply(arr, _toConsumableArray(item.separator));
      if (next.type === KEY || item.type !== KEY && (item.type === SINGLELINE || next.type === SINGLELINE)) {
        arr.push(createBr(), createIndent(level));
      } else {
        arr.push(createSpace(1));
      }
    }
  }
  return arr;
}

function createNodes(data) {
  var valueType = typeof data.value;
  var type = data.type || valueType;
  var el = createElement('span', {
    className: data.cls || `item ${type}`,
    textContent: `${data.value}`
  });
  el.dataset.type = valueType;
  el.dataset.value = data.value;
  var els = [el];
  if (data.type === 'key' || !data.cls && type === 'string') {
    els.unshift(createQuote());
    els.push(createQuote());
  }
  return els;
}

function render(data) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

  if (Array.isArray(data)) {
    var arr = [];
    var ret = {
      type: MULTILINE,
      separator: [createComma()]
    };
    arr.push.apply(arr, _toConsumableArray(createNodes({ value: '[', cls: 'bracket' })));
    if (data.length) {
      var rendered = data.reduce(function (res, item) {
        return [].concat(_toConsumableArray(res), [render(item, level + 1)]);
      }, []);
      arr.push.apply(arr, [createBr(), createIndent(level + 1)].concat(_toConsumableArray(join(rendered, level + 1)), [createBr(), createIndent(level)]));
    } else {
      arr.push.apply(arr, _toConsumableArray(createNodes({ value: '', cls: 'subtle' })));
      ret.type = SINGLELINE;
    }
    arr.push.apply(arr, _toConsumableArray(createNodes({ value: ']', cls: 'bracket' })));
    ret.data = arr;
    return ret;
  }
  if (data === null) {
    return {
      type: SINGLELINE,
      separator: [createComma()],
      data: createNodes({ value: data, type: 'null' })
    };
  }
  if (typeof data === 'object') {
    var _arr = [];
    var _ret = {
      type: MULTILINE,
      separator: [createComma()]
    };
    _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '{', cls: 'bracket' })));
    var _rendered = Object.keys(data).reduce(function (res, key) {
      return res.concat([{
        type: KEY,
        data: createNodes({ value: key, type: 'key' }),
        separator: createNodes({ value: ':', cls: 'subtle' })
      }, render(data[key], level + 1)]);
    }, []);
    if (_rendered.length) {
      _arr.push.apply(_arr, [createBr(), createIndent(level + 1)].concat(_toConsumableArray(join(_rendered, level + 1)), [createBr(), createIndent(level)]));
    } else {
      _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '', cls: 'subtle' })));
      _ret.type = SINGLELINE;
    }
    _arr.push.apply(_arr, _toConsumableArray(createNodes({ value: '}', cls: 'bracket' })));
    _ret.data = _arr;
    return _ret;
  }
  return {
    type: SINGLELINE,
    separator: [createComma()],
    data: createNodes({ value: data })
  };
}

function loadJSON() {
  var text = document.body.innerText;
  try {
    // JSON
    var content = JSON.parse(text);
    return { prefix: '', suffix: '', content };
  } catch (e) {
    // not JSON
  }
  try {
    // JSONP
    var parts = text.match(/^(.*?\w\s*\()(.+)(\)[;\s]*)$/);
    var _content = JSON.parse(parts[2]);
    var prefix = parts[1];
    var suffix = parts[3];
    return { prefix, content: _content, suffix };
  } catch (e) {
    // not JSONP
  }
}

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 = loadJSON();
      if (!formatter.data) return;
      // formatter.style = GM_addStyle(".tips-link {\n    color: slateblue;\n}.tips-val {\n    color: dodgerblue;\n}* {\n  margin: 0;\n  padding: 0;\n}\n\n#root {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  margin: 0;\n  padding: 16px;\n  font-family: Menlo, \"Microsoft YaHei\", Tahoma;\n  font-size: 14px;\n  overflow: auto;\n}\n\n#root > pre {\n    white-space: pre-wrap;\n}\n\n.subtle {\n  color: #999;\n}\n.number {\n  color: darkorange;\n}\n.null {\n  color: gray;\n}\n.key {\n  color: brown;\n}\n.string {\n  color: green;\n}\n.boolean {\n  color: dodgerblue;\n}\n.bracket {\n  color: blue;\n}\n.item {\n  cursor: pointer;\n}\n\n.tips {\n  position: absolute;\n  padding: .5em;\n  border-radius: .5em;\n  box-shadow: 0 0 1em gray;\n  background: white;\n  z-index: 1;\n  white-space: nowrap;\n  color: black\n}\n\n.tips-key {\n    font-weight: bold;\n}\n.menu {\n  position: fixed;\n  top: 0;\n  right: 0;\n  background: white;\n  padding: 5px;\n  user-select: none;\n}\n.menu > span {\n    margin-right: 5px;\n}\n.menu .btn {\n    display: inline-block;\n    width: 18px;\n    height: 18px;\n    line-height: 18px;\n    text-align: center;\n    background: #ddd;\n    border-radius: 4px;\n    cursor: pointer\n}\n.menu .btn.active {\n    color: white;\n    background: #444;\n}\n\n.hide-quotes .quote, .hide-commas .comma {\n  font-size: 0;\n}\n\n.space {\n  letter-spacing: 8px;\n}\n");
      initTips();
      initMenu();
      formatter.render = function () {
        var pre = formatter.pre;
        var _formatter$data = formatter.data,
            prefix = _formatter$data.prefix,
            content = _formatter$data.content,
            suffix = _formatter$data.suffix;

        pre.innerHTML = '';
        [createElement('span', {
          className: 'subtle',
          textContent: prefix
        })].concat(_toConsumableArray(render(content).data), [createElement('span', {
          className: 'subtle',
          textContent: suffix
        })]).forEach(function (el) {
          pre.appendChild(el);
        });
        formatter.update();
      };
      formatter.update = function () {
        formatter.options.forEach(function (_ref) {
          var key = _ref.key;

          formatter.pre.classList[config[key] ? 'add' : 'remove'](key);
        });
      };
    }
    formatter.formatted = true;
    var hostRoot = createElement('div');
    document.body.innerHTML = '';
    document.body.appendChild(hostRoot);
    var shadow = hostRoot.attachShadow({ mode: 'open' });
    formatter.style = createElement('style', {
      textContent: ".tips-link {\n    color: slateblue;\n}.tips-val {\n    color: dodgerblue;\n}* {\n  margin: 0;\n  padding: 0;\n}\n\n#root {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  margin: 0;\n  padding: 16px;\n  font-family: Menlo, \"Microsoft YaHei\", Tahoma;\n  font-size: 14px;\n  overflow: auto;\n}\n\n#root > pre {\n    white-space: pre-wrap;\n}\n\n.subtle {\n  color: #999;\n}\n.number {\n  color: darkorange;\n}\n.null {\n  color: gray;\n}\n.key {\n  color: brown;\n}\n.string {\n  color: green;\n}\n.boolean {\n  color: dodgerblue;\n}\n.bracket {\n  color: blue;\n}\n.item {\n  cursor: pointer;\n}\n\n.tips {\n  position: absolute;\n  padding: .5em;\n  border-radius: .5em;\n  box-shadow: 0 0 1em gray;\n  background: white;\n  z-index: 1;\n  white-space: nowrap;\n  color: black\n}\n\n.tips-key {\n    font-weight: bold;\n}\n.menu {\n  position: fixed;\n  top: 0;\n  right: 0;\n  background: white;\n  padding: 5px;\n  user-select: none;\n}\n.menu > span {\n    margin-right: 5px;\n}\n.menu .btn {\n    display: inline-block;\n    width: 18px;\n    height: 18px;\n    line-height: 18px;\n    text-align: center;\n    background: #ddd;\n    border-radius: 4px;\n    cursor: pointer\n}\n.menu .btn.active {\n    color: white;\n    background: #444;\n}\n\n.hide-quotes .quote, .hide-commas .comma {\n  font-size: 0;\n}\n\n.space {\n  letter-spacing: 8px;\n}\n"
    });
    shadow.appendChild(formatter.style);
    formatter.root = createElement('div', { id: 'root' });
    shadow.appendChild(formatter.root);
    formatter.pre = createElement('pre');
    formatter.root.appendChild(formatter.pre);
    formatter.menu.attach();
    bindEvents();
    formatter.render();
  }
}

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

function initMenu() {
  var menu = createElement('div', {
    className: 'menu'
  });
  formatter.options.forEach(function (item) {
    var span = createElement('span', {
      className: `btn${config[item.key] ? ' active' : ''}`,
      innerHTML: item.title
    });
    span.dataset.key = item.key;
    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() {
      formatter.root.appendChild(menu);
    },
    detach() {
      removeEl(menu);
    }
  };
}

function initTips() {
  var tips = createElement('div', {
    className: 'tips'
  });
  var hide = function hide() {
    return removeEl(tips);
  };
  tips.addEventListener('click', function (e) {
    e.stopPropagation();
  }, false);
  document.addEventListener('click', hide, false);
  formatter.tips = {
    node: tips,
    hide,
    show(range) {
      var scrollTop = document.body.scrollTop;
      var rects = range.getClientRects();
      var rect = void 0;
      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`;
      var _range$startContainer = range.startContainer.dataset,
          type = _range$startContainer.type,
          value = _range$startContainer.value;

      var html = [`<span class="tips-key">type</span>: <span class="tips-val">${safeHTML(type)}</span>`];
      if (type === 'string' && /^(https?|ftps?):\/\/\S+/.test(value)) {
        html.push('<br>', `<a class="tips-link" href="${encodeURI(value)}" target="_blank">Open link</a>`);
      }
      tips.innerHTML = html.join('');
      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('item')) {
      formatter.tips.show(selectNode(target));
    } else {
      formatter.tips.hide();
    }
  }, false);
}