/**
* @author n1474335 [n1474335@gmail.com]
* @author Phillip Nordwall [phillip.nordwall@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import JSON5 from "json5";
import OperationError from "../errors/OperationError.mjs";
import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
/**
* JSON Beautify operation
*/
class JSONBeautify extends Operation {
/**
* JSONBeautify constructor
*/
constructor() {
super();
this.name = "JSON Beautify";
this.module = "Code";
this.description = "Indents and pretty prints JavaScript Object Notation (JSON) code.
Tags: json viewer, prettify, syntax highlighting";
this.inputType = "string";
this.outputType = "string";
this.presentType = "html";
this.args = [
{
name: "Indent string",
type: "binaryShortString",
value: " "
},
{
name: "Sort Object Keys",
type: "boolean",
value: false
},
{
name: "Formatted",
type: "boolean",
value: true
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (!input) return "";
const [indentStr, sortBool] = args;
let json = null;
try {
json = JSON5.parse(input);
} catch (err) {
throw new OperationError("Unable to parse input as JSON.\n" + err);
}
if (sortBool) json = sortKeys(json);
return JSON.stringify(json, null, indentStr);
}
/**
* Adds various dynamic features to the JSON blob
*
* @param {string} data
* @param {Object[]} args
* @returns {html}
*/
present(data, args) {
const formatted = args[2];
if (!formatted) return Utils.escapeHtml(data);
const json = JSON5.parse(data);
const options = {
withLinks: true,
bigNumbers: true
};
let html = '
';
if (isCollapsable(json)) {
const isArr = json instanceof Array;
html += '' +
`
` +
json2html(json, options) +
" ";
} else {
html += json2html(json, options);
}
html += "
";
return html;
}
}
/**
* Sort keys in a JSON object
*
* @author Phillip Nordwall [phillip.nordwall@gmail.com]
* @param {object} o
* @returns {object}
*/
function sortKeys(o) {
if (Array.isArray(o)) {
return o.map(sortKeys);
} else if ("[object Object]" === Object.prototype.toString.call(o)) {
return Object.keys(o).sort().reduce(function(a, k) {
a[k] = sortKeys(o[k]);
return a;
}, {});
}
return o;
}
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @returns {boolean}
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string looks like a URL, based on protocol
* @returns {boolean}
*/
function isUrl(string) {
const protocols = ["http", "https", "ftp", "ftps"];
for (let i = 0; i < protocols.length; i++) {
if (string.startsWith(protocols[i] + "://")) {
return true;
}
}
return false;
}
/**
* Transform a json object into html representation
*
* Adapted for CyberChef by @n1474335 from jQuery json-viewer
* @author Alexandre Bodelot
* @link https://github.com/abodelot/jquery.json-viewer
* @license MIT
*
* @returns {string}
*/
function json2html(json, options) {
let html = "";
if (typeof json === "string") {
// Escape tags and quotes
json = Utils.escapeHtml(json);
if (options.withLinks && isUrl(json)) {
html += `${json}`;
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/"/g, "\\"");
html += `"${json}"`;
}
} else if (typeof json === "number" || typeof json === "bigint") {
html += `${json}`;
} else if (typeof json === "boolean") {
html += `${json}`;
} else if (json === null) {
html += 'null';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[';
for (let i = 0; i < json.length; i++) {
html += "- ";
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
const isArr = json[i] instanceof Array;
html += '
' +
`
` +
json2html(json[i], options) +
" ";
} else {
html += json2html(json[i], options);
}
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += " ";
}
html += '
]';
} else {
html += '[]';
}
} else if (typeof json === "object") {
// Optional support different libraries for big numbers
// json.isLosslessNumber: package lossless-json
// json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others?
if (options.bigNumbers && (typeof json.toExponential === "function" || json.isLosslessNumber)) {
html += `${json.toString()}`;
} else {
let keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{';
for (const key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
const safeKey = Utils.escapeHtml(key);
html += "- ";
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
const isArr = json[key] instanceof Array;
html += '
' +
`${safeKey}:
` +
json2html(json[key], options) +
" ";
} else {
html += safeKey + ': ' + json2html(json[key], options);
}
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += " ";
}
}
html += '
}';
} else {
html += '{}';
}
}
}
return html;
}
export default JSONBeautify;