'JSON Beautify' operation now supports formatting, collapsing and syntax highlighting. Closes #203.

This commit is contained in:
n1474335 2022-06-08 18:06:41 +01:00
parent 4274e8f3a2
commit 5349115b94
6 changed files with 304 additions and 41 deletions

5
package-lock.json generated
View file

@ -45,6 +45,7 @@
"js-crc": "^0.2.0", "js-crc": "^0.2.0",
"js-sha3": "^0.8.0", "js-sha3": "^0.8.0",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
"json5": "^2.2.1",
"jsonpath": "^1.1.1", "jsonpath": "^1.1.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
@ -9489,7 +9490,6 @@
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true,
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
}, },
@ -23014,8 +23014,7 @@
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"dev": true
}, },
"jsonpath": { "jsonpath": {
"version": "1.1.1", "version": "1.1.1",

View file

@ -122,6 +122,7 @@
"js-crc": "^0.2.0", "js-crc": "^0.2.0",
"js-sha3": "^0.8.0", "js-sha3": "^0.8.0",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
"json5": "^2.2.1",
"jsonpath": "^1.1.1", "jsonpath": "^1.1.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",

View file

@ -5,8 +5,10 @@
* @license Apache-2.0 * @license Apache-2.0
*/ */
import vkbeautify from "vkbeautify"; import JSON5 from "json5";
import OperationError from "../errors/OperationError.mjs";
import Operation from "../Operation.mjs"; import Operation from "../Operation.mjs";
import Utils from "../Utils.mjs";
/** /**
* JSON Beautify operation * JSON Beautify operation
@ -21,19 +23,25 @@ class JSONBeautify extends Operation {
this.name = "JSON Beautify"; this.name = "JSON Beautify";
this.module = "Code"; this.module = "Code";
this.description = "Indents and prettifies JavaScript Object Notation (JSON) code."; this.description = "Indents and pretty prints JavaScript Object Notation (JSON) code.<br><br>Tags: json viewer, prettify, syntax highlighting";
this.inputType = "string"; this.inputType = "string";
this.outputType = "string"; this.outputType = "string";
this.presentType = "html";
this.args = [ this.args = [
{ {
"name": "Indent string", name: "Indent string",
"type": "binaryShortString", type: "binaryShortString",
"value": " " value: " "
}, },
{ {
"name": "Sort Object Keys", name: "Sort Object Keys",
"type": "boolean", type: "boolean",
"value": false value: false
},
{
name: "Formatted",
type: "boolean",
value: true
} }
]; ];
} }
@ -44,35 +52,193 @@ class JSONBeautify extends Operation {
* @returns {string} * @returns {string}
*/ */
run(input, args) { run(input, args) {
const [indentStr, sortBool] = args;
if (!input) return ""; if (!input) return "";
if (sortBool) {
input = JSON.stringify(JSONBeautify._sort(JSON.parse(input))); const [indentStr, sortBool] = args;
} let json = null;
return vkbeautify.json(input, indentStr);
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);
}
/** /**
* Sort JSON representation of an object * 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 = '<div class="json-document">';
if (isCollapsable(json)) {
const isArr = json instanceof Array;
html += '<details open class="json-details">' +
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}"></summary>` +
json2html(json, options) +
"</details>";
} else {
html += json2html(json, options);
}
html += "</div>";
return html;
}
}
/**
* Sort keys in a JSON object
* *
* @author Phillip Nordwall [phillip.nordwall@gmail.com] * @author Phillip Nordwall [phillip.nordwall@gmail.com]
* @private
* @param {object} o * @param {object} o
* @returns {object} * @returns {object}
*/ */
static _sort(o) { function sortKeys(o) {
if (Array.isArray(o)) { if (Array.isArray(o)) {
return o.map(JSONBeautify._sort); return o.map(sortKeys);
} else if ("[object Object]" === Object.prototype.toString.call(o)) { } else if ("[object Object]" === Object.prototype.toString.call(o)) {
return Object.keys(o).sort().reduce(function(a, k) { return Object.keys(o).sort().reduce(function(a, k) {
a[k] = JSONBeautify._sort(o[k]); a[k] = sortKeys(o[k]);
return a; return a;
}, {}); }, {});
} }
return o; 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 <alexandre.bodelot@gmail.com>
* @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 += `<a href="${json}" class="json-string" target="_blank">${json}</a>`;
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, "\\&quot;");
html += `<span class="json-string">"${json}"</span>`;
}
} else if (typeof json === "number" || typeof json === "bigint") {
html += `<span class="json-literal">${json}</span>`;
} else if (typeof json === "boolean") {
html += `<span class="json-literal">${json}</span>`;
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '<span class="json-bracket">[</span><ol class="json-array">';
for (let i = 0; i < json.length; i++) {
html += "<li>";
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
const isArr = json[i] instanceof Array;
html += '<details open class="json-details">' +
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}"></summary>` +
json2html(json[i], options) +
"</details>";
} else {
html += json2html(json[i], options);
}
// Add comma if item is not last
if (i < json.length - 1) {
html += '<span class="json-comma">,</span>';
}
html += "</li>";
}
html += '</ol><span class="json-bracket">]</span>';
} else {
html += '<span class="json-bracket">[]</span>';
}
} 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 += `<span class="json-literal">${json.toString()}</span>`;
} else {
let keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '<span class="json-brace">{</span><ul class="json-dict">';
for (const key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
const safeKey = Utils.escapeHtml(key);
html += "<li>";
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
const isArr = json[key] instanceof Array;
html += '<details open class="json-details">' +
`<summary class="json-summary ${isArr ? "json-arr" : "json-obj"}">${safeKey}<span class="json-colon">:</span> </summary>` +
json2html(json[key], options) +
"</details>";
} else {
html += safeKey + '<span class="json-colon">:</span> ' + json2html(json[key], options);
}
// Add comma if item is not last
if (--keyCount > 0) {
html += '<span class="json-comma">,</span>';
}
html += "</li>";
}
}
html += '</ul><span class="json-brace">}</span>';
} else {
html += '<span class="json-brace">{}</span>';
}
}
}
return html;
} }
export default JSONBeautify; export default JSONBeautify;

View file

@ -34,3 +34,6 @@
@import "./layout/_operations.css"; @import "./layout/_operations.css";
@import "./layout/_recipe.css"; @import "./layout/_recipe.css";
@import "./layout/_structure.css"; @import "./layout/_structure.css";
/* Operations */
@import "./operations/json.css";

View file

@ -0,0 +1,78 @@
/**
* JSON styles
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2022
* @license Apache-2.0
*
* Adapted for CyberChef by @n1474335 from jQuery json-viewer
* @author Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link https://github.com/abodelot/jquery.json-viewer
* @license MIT
*/
/* Root element */
.json-document {
padding: .5em 1.5em;
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: green;
}
.json-literal {
color: red;
}
.json-brace,
.json-bracket,
.json-colon,
.json-comma {
color: gray;
}
/* Collapse */
.json-details {
display: inline;
}
.json-details[open] {
display: contents;
}
.json-summary {
display: contents;
}
/* Display object and array brackets when closed */
.json-summary.json-obj::after {
color: gray;
content: "{ ... }"
}
.json-summary.json-arr::after {
color: gray;
content: "[ ... ]"
}
.json-details[open] > .json-summary.json-obj::after,
.json-details[open] > .json-summary.json-arr::after {
content: "";
}
/* Show arrows, even in inline mode */
.json-summary::before {
content: "\25BC";
color: #c0c0c0;
margin-left: -12px;
margin-right: 5px;
display: inline-block;
transform: rotate(-90deg);
}
.json-summary:hover::before {
color: #aaa;
}
.json-details[open] > .json-summary::before {
transform: rotate(0deg);
}

View file

@ -16,7 +16,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
], ],
}, },
@ -27,7 +27,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
], ],
}, },
@ -38,8 +38,12 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
{
op: "HTML To Text",
args: []
}
], ],
}, },
{ {
@ -49,7 +53,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
], ],
}, },
@ -60,7 +64,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
], ],
}, },
@ -71,7 +75,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
], ],
}, },
@ -82,7 +86,7 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: ["\t", false], args: ["\t", false, false],
}, },
], ],
}, },
@ -93,8 +97,12 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: [" ", false], args: [" ", false, false],
}, },
{
op: "HTML To Text",
args: []
}
], ],
}, },
{ {
@ -104,8 +112,12 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: ["\t", false], args: ["\t", false, false],
}, },
{
op: "HTML To Text",
args: []
}
], ],
}, },
{ {
@ -115,8 +127,12 @@ TestRegister.addTests([
recipeConfig: [ recipeConfig: [
{ {
op: "JSON Beautify", op: "JSON Beautify",
args: ["\t", true], args: ["\t", true, false],
}, },
{
op: "HTML To Text",
args: []
}
], ],
}, },
]); ]);