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 += "- ";
+
+ // 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;
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: []
+ }
],
},
]);