JSON formatter

Format JSON data in a beautiful way.

目前為 2017-11-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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