JSON formatter

Format JSON data in a beautiful way.

13.11.2017 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

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

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

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

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

(Zateb bir user-style yöneticim var, yükleyeyim!)

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