From 5349115b947bae9d66f6c1cd9d540da150a2dac7 Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 8 Jun 2022 18:06:41 +0100 Subject: [PATCH] 'JSON Beautify' operation now supports formatting, collapsing and syntax highlighting. Closes #203. --- package-lock.json | 5 +- package.json | 1 + src/core/operations/JSONBeautify.mjs | 222 +++++++++++++++++++++--- src/web/stylesheets/index.css | 3 + src/web/stylesheets/operations/json.css | 78 +++++++++ tests/operations/tests/JSONBeautify.mjs | 36 ++-- 6 files changed, 304 insertions(+), 41 deletions(-) create mode 100644 src/web/stylesheets/operations/json.css diff --git a/package-lock.json b/package-lock.json index ff6dfe1f..cd04e5d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "js-crc": "^0.2.0", "js-sha3": "^0.8.0", "jsesc": "^3.0.2", + "json5": "^2.2.1", "jsonpath": "^1.1.1", "jsonwebtoken": "^8.5.1", "jsqr": "^1.4.0", @@ -9489,7 +9490,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -23014,8 +23014,7 @@ "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, "jsonpath": { "version": "1.1.1", diff --git a/package.json b/package.json index 47b4678d..72208f29 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "js-crc": "^0.2.0", "js-sha3": "^0.8.0", "jsesc": "^3.0.2", + "json5": "^2.2.1", "jsonpath": "^1.1.1", "jsonwebtoken": "^8.5.1", "jsqr": "^1.4.0", diff --git a/src/core/operations/JSONBeautify.mjs b/src/core/operations/JSONBeautify.mjs index f53d86e6..923ae7dc 100644 --- a/src/core/operations/JSONBeautify.mjs +++ b/src/core/operations/JSONBeautify.mjs @@ -5,8 +5,10 @@ * @license Apache-2.0 */ -import vkbeautify from "vkbeautify"; +import JSON5 from "json5"; +import OperationError from "../errors/OperationError.mjs"; import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; /** * JSON Beautify operation @@ -21,19 +23,25 @@ class JSONBeautify extends Operation { this.name = "JSON Beautify"; this.module = "Code"; - this.description = "Indents and prettifies JavaScript Object Notation (JSON) 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: "Indent string", + type: "binaryShortString", + value: " " }, { - "name": "Sort Object Keys", - "type": "boolean", - "value": false + name: "Sort Object Keys", + type: "boolean", + value: false + }, + { + name: "Formatted", + type: "boolean", + value: true } ]; } @@ -44,35 +52,193 @@ class JSONBeautify extends Operation { * @returns {string} */ run(input, args) { - const [indentStr, sortBool] = args; - if (!input) return ""; - if (sortBool) { - input = JSON.stringify(JSONBeautify._sort(JSON.parse(input))); + + 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); } - return vkbeautify.json(input, indentStr); + + 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 * - * @author Phillip Nordwall [phillip.nordwall@gmail.com] - * @private - * @param {object} o - * @returns {object} + * @param {string} data + * @param {Object[]} args + * @returns {html} */ - static _sort(o) { - if (Array.isArray(o)) { - return o.map(JSONBeautify._sort); - } else if ("[object Object]" === Object.prototype.toString.call(o)) { - return Object.keys(o).sort().reduce(function(a, k) { - a[k] = JSONBeautify._sort(o[k]); - return a; - }, {}); + 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); } - return o; + + 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 += "
  1. "; + + // 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 += "
  2. "; + } + 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 += '{}'; + } else { + html += '{}'; + } + } + } + return html; +} + export default JSONBeautify; diff --git a/src/web/stylesheets/index.css b/src/web/stylesheets/index.css index ef35d54f..960c7006 100755 --- a/src/web/stylesheets/index.css +++ b/src/web/stylesheets/index.css @@ -34,3 +34,6 @@ @import "./layout/_operations.css"; @import "./layout/_recipe.css"; @import "./layout/_structure.css"; + +/* Operations */ +@import "./operations/json.css"; diff --git a/src/web/stylesheets/operations/json.css b/src/web/stylesheets/operations/json.css new file mode 100644 index 00000000..22c07128 --- /dev/null +++ b/src/web/stylesheets/operations/json.css @@ -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 + * @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); +} diff --git a/tests/operations/tests/JSONBeautify.mjs b/tests/operations/tests/JSONBeautify.mjs index 11578678..211f96bb 100644 --- a/tests/operations/tests/JSONBeautify.mjs +++ b/tests/operations/tests/JSONBeautify.mjs @@ -16,7 +16,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, ], }, @@ -27,7 +27,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, ], }, @@ -38,8 +38,12 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, + { + op: "HTML To Text", + args: [] + } ], }, { @@ -49,7 +53,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, ], }, @@ -60,7 +64,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, ], }, @@ -71,7 +75,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, ], }, @@ -82,7 +86,7 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: ["\t", false], + args: ["\t", false, false], }, ], }, @@ -93,8 +97,12 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: [" ", false], + args: [" ", false, false], }, + { + op: "HTML To Text", + args: [] + } ], }, { @@ -104,8 +112,12 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: ["\t", false], + args: ["\t", false, false], }, + { + op: "HTML To Text", + args: [] + } ], }, { @@ -115,8 +127,12 @@ TestRegister.addTests([ recipeConfig: [ { op: "JSON Beautify", - args: ["\t", true], + args: ["\t", true, false], }, + { + op: "HTML To Text", + args: [] + } ], }, ]);