JSON formatter

Format JSON data in a beautiful way.

Versione datata 13/11/2017. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

'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);
}