Explorar o código

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

n1474335 %!s(int64=3) %!d(string=hai) anos
pai
achega
5349115b94

+ 2 - 3
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",

+ 1 - 0
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",

+ 193 - 27
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.<br><br>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) {
+        if (!input) return "";
+
         const [indentStr, sortBool] = args;
+        let json = null;
 
-        if (!input) return "";
-        if (sortBool) {
-            input = JSON.stringify(JSONBeautify._sort(JSON.parse(input)));
+        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 = '<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]
+ * @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 <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 o;
     }
+    return html;
 }
 
 export default JSONBeautify;

+ 3 - 0
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";

+ 78 - 0
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 <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);
+}

+ 26 - 10
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: []
+            }
         ],
     },
 ]);