// Copyright © 2013-2017 David Caldwell <david@porkrind.org>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// Usage
// -----
// The module exports one entry point, the `renderjson()` function. It takes in
// the JSON you want to render as a single argument and returns an HTML
// element.
//
// Options
// -------
// renderjson.set_icons("+", "-")
// This Allows you to override the disclosure icons.
//
// renderjson.set_show_to_level(level)
// Pass the number of levels to expand when rendering. The default is 0, which
// starts with everything collapsed. As a special case, if level is the string
// "all" then it will start with everything expanded.
//
// renderjson.set_max_string_length(length)
// Strings will be truncated and made expandable if they are longer than
// `length`. As a special case, if `length` is the string "none" then
// there will be no truncation. The default is "none".
//
// renderjson.set_sort_objects(sort_bool)
// Sort objects by key (default: false)
//
// renderjson.set_replacer(replacer_function)
// Equivalent of JSON.stringify() `replacer` argument when it's a function
//
// renderjson.set_collapse_msg(collapse_function)
// Accepts a function (len:number):string => {} where len is the length of the
// object collapsed. Function should return the message displayed when a
// object is collapsed. The default message is "X items"
//
// renderjson.set_property_list(property_list)
// Equivalent of JSON.stringify() `replacer` argument when it's an array
//
// Theming
// -------
// The HTML output uses a number of classes so that you can theme it the way
// you'd like:
// .disclosure ("⊕", "⊖")
// .syntax (",", ":", "{", "}", "[", "]")
// .string (includes quotes)
// .number
// .boolean
// .key (object key)
// .keyword ("null", "undefined")
// .object.syntax ("{", "}")
// .array.syntax ("[", "]")
const renderjson = (function () {
const themetext = function (/* [class, text]+ */) {
const spans = [];
while (arguments.length)
spans.push(
append(
span(Array.prototype.shift.call(arguments)),
text(Array.prototype.shift.call(arguments))
)
);
return spans;
};
const append = function (/* el, ... */) {
const el = Array.prototype.shift.call(arguments);
for (let a = 0; a < arguments.length; a++)
if (arguments[a].constructor == Array)
append.apply(this, [el].concat(arguments[a]));
else el.appendChild(arguments[a]);
return el;
};
const prepend = function (el, child) {
el.insertBefore(child, el.firstChild);
return el;
};
const isempty = function (obj, pl) {
const keys = pl || Object.keys(obj);
for (const i in keys)
if (Object.hasOwnProperty.call(obj, keys[i])) return false;
return true;
};
const text = function (txt) {
return document.createTextNode(txt);
};
const span = function (classname) {
const s = document.createElement('span');
if (classname) s.className = classname;
return s;
};
const A = function A(txt, classname, callback) {
const a = document.createElement('a');
if (classname) a.className = classname;
a.appendChild(text(txt));
a.href = '#';
a.onclick = function (e) {
callback();
if (e) e.stopPropagation();
return false;
};
return a;
};
/**
* @typedef {object} OptionsRenderJSON
* @property {string} hide todo
* @property {string} show todo
* @property {number} max_string_length The maximum length of a string to be displayed.
* @property {Function} collapse_msg A function that takes in a number and returns a string.
* @property {Function} replacer A function that takes in a key and a value and returns a value.
* @property {Array} property_list An array of properties to be displayed.
* @property {number} show_to_level The number of levels to expand when rendering. The default is 0, which
starts with everything collapsed. As a special case, if level is the string
"all" then it will start with everything expanded.
*/
/**
* It takes a JSON object and returns a DOM element that renders the JSON object
*
* @param {object} json - The JSON object to be rendered
* @param {string} indent - The indentation string to use.
* @param {string} dont_indent - If true, don't indent the current line.
* @param {number} show_level - The number of levels to show.
* @param {OptionsRenderJSON} options - Options of render
* @returns {string} A function that takes in a json object and returns a string.
*/
function _renderjson(json, indent, dont_indent, show_level, options) {
const my_indent = dont_indent ? '' : indent;
const disclosure = function (open, placeholder, close, type, builder) {
let content;
const empty = span(type);
const show = function () {
if (!content)
append(
empty.parentNode,
(content = prepend(
builder(),
A(options.hide, 'disclosure', function () {
content.style.display = 'none';
empty.style.display = 'inline';
})
))
);
content.style.display = 'inline';
empty.style.display = 'none';
};
append(
empty,
A(options.show, 'disclosure', show),
themetext(type + ' syntax', open),
A(placeholder, null, show),
themetext(type + ' syntax', close)
);
const el = append(span(), text(my_indent.slice(0, -1)), empty);
if (show_level > 0 && type != 'string') show();
return el;
};
if (json === null) return themetext(null, my_indent, 'keyword', 'null');
if (json === void 0)
return themetext(null, my_indent, 'keyword', 'undefined');
if (typeof json == 'string' && json.length > options.max_string_length)
return disclosure(
'"',
json.substr(0, options.max_string_length) + ' ...',
'"',
'string',
function () {
return append(
span('string'),
themetext(null, my_indent, 'string', JSON.stringify(json))
);
}
);
if (
typeof json != 'object' ||
[Number, String, Boolean, Date].indexOf(json.constructor) >= 0
)
// Strings, numbers and bools
return themetext(null, my_indent, typeof json, JSON.stringify(json));
if (json.constructor == Array) {
if (json.length == 0)
return themetext(null, my_indent, 'array syntax', '[]');
return disclosure(
'[',
options.collapse_msg(json.length),
']',
'array',
function () {
const as = append(
span('array'),
themetext('array syntax', '[', null, '\n')
);
for (let i = 0; i < json.length; i++)
append(
as,
_renderjson(
options.replacer.call(json, i, json[i]),
indent + ' ',
false,
show_level - 1,
options
),
i != json.length - 1 ? themetext('syntax', ',') : [],
text('\n')
);
append(as, themetext(null, indent, 'array syntax', ']'));
return as;
}
);
}
// Object
if (isempty(json, options.property_list))
return themetext(null, my_indent, 'object syntax', '{}');
return disclosure(
'{',
options.collapse_msg(Object.keys(json).length),
'}',
'object',
function () {
const os = append(
span('object'),
themetext('object syntax', '{', null, '\n')
);
let last;
for (const k in json) last = k;
let keys = options.property_list || Object.keys(json);
if (options.sort_objects) keys = keys.sort();
for (const i in keys) {
const k = keys[i];
if (!(k in json)) continue;
append(
os,
themetext(
null,
indent + ' ',
'key',
'"' + k + '"',
'object syntax',
': '
),
_renderjson(
options.replacer.call(json, k, json[k]),
indent + ' ',
true,
show_level - 1,
options
),
k != last ? themetext('syntax', ',') : [],
text('\n')
);
}
append(os, themetext(null, indent, 'object syntax', '}'));
return os;
}
);
}
const renderjson = function renderjson(json) {
/** @type {OptionsRenderJSON} */
const options = new Object(renderjson.options); // eslint-disable-line no-new-object
options.replacer =
typeof options.replacer == 'function'
? options.replacer
: function (k, v) {
return v;
};
const pre = append(
document.createElement('pre'),
_renderjson(json, '', false, options.show_to_level, options)
);
pre.className = 'renderjson';
return pre;
};
renderjson.set_icons = function (show, hide) {
renderjson.options.show = show;
renderjson.options.hide = hide;
return renderjson;
};
renderjson.set_show_to_level = function (level) {
renderjson.options.show_to_level =
typeof level == 'string' && level.toLowerCase() === 'all'
? Number.MAX_VALUE
: level;
return renderjson;
};
renderjson.set_max_string_length = function (length) {
renderjson.options.max_string_length =
typeof length == 'string' && length.toLowerCase() === 'none'
? Number.MAX_VALUE
: length;
return renderjson;
};
renderjson.set_sort_objects = function (sort_bool) {
renderjson.options.sort_objects = sort_bool;
return renderjson;
};
renderjson.set_replacer = function (replacer) {
renderjson.options.replacer = replacer;
return renderjson;
};
renderjson.set_collapse_msg = function (collapse_msg) {
renderjson.options.collapse_msg = collapse_msg;
return renderjson;
};
renderjson.set_property_list = function (prop_list) {
renderjson.options.property_list = prop_list;
return renderjson;
};
// Backwards compatiblity. Use set_show_to_level() for new code.
renderjson.set_show_by_default = function (show) {
renderjson.options.show_to_level = show ? Number.MAX_VALUE : 0;
return renderjson;
};
renderjson.options = {};
renderjson.set_icons('⊕', '⊖');
renderjson.set_show_by_default(false);
renderjson.set_sort_objects(false);
renderjson.set_max_string_length('none');
renderjson.set_replacer(void 0);
renderjson.set_property_list(void 0);
renderjson.set_collapse_msg(function (len) {
return len + ' item' + (len == 1 ? '' : 's');
});
return renderjson;
})();
export class JsonRenderer {
constructor() {
this.renderjson = renderjson;
}
}