From e2af3c78e7550502ec0d7384af29f4302a539306 Mon Sep 17 00:00:00 2001 From: Mark Jones Date: Thu, 26 Apr 2018 00:33:19 +0100 Subject: [PATCH 1/3] Added ToTable operation to output data as ASCII or HTML tables. --- src/core/config/Categories.js | 1 + src/core/config/OperationConfig.js | 33 +++++ src/core/config/modules/Default.js | 2 + src/core/operations/ToTable.js | 189 +++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+) create mode 100755 src/core/operations/ToTable.js diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index b2c404f6..33ec4a99 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -67,6 +67,7 @@ const Categories = [ "Encode text", "Decode text", "Swap endianness", + "To Table", ] }, { diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index bbe80a09..5d2ca478 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -37,6 +37,7 @@ import SeqUtils from "../operations/SeqUtils.js"; import Shellcode from "../operations/Shellcode.js"; import StrUtils from "../operations/StrUtils.js"; import Tidy from "../operations/Tidy.js"; +import ToTable from "../operations/ToTable.js"; import Unicode from "../operations/Unicode.js"; import URL_ from "../operations/URL.js"; @@ -613,6 +614,38 @@ const OperationConfig = { } ] }, + "To Table": { + module: "Default", + description: "Renders data as a table. Data can be split on different characters and output as a HTML or ASCII table with optional header row.", + inputType: "string", + outputType: "html", + highlight: false, + highlightReverse: false, + manualBake: false, + args: [ + { + name: "Select separator", + type: "populateOption", + value: ToTable.SEPARATORS, + target: 1 + }, + { + name: "Separator", + type: "string", + value: "," + }, + { + name: "First row header?", + type: "boolean", + value: false + }, + { + name: "Format", + type: "option", + value: ToTable.FORMATS + } + ] + }, "From Hex": { module: "Default", description: "Converts a hexadecimal byte string back into its raw value.

e.g. ce 93 ce b5 ce b9 ce ac 20 cf 83 ce bf cf 85 0a becomes the UTF-8 encoded string Γειά σου", diff --git a/src/core/config/modules/Default.js b/src/core/config/modules/Default.js index e5f070cf..6b9f60f9 100644 --- a/src/core/config/modules/Default.js +++ b/src/core/config/modules/Default.js @@ -27,6 +27,7 @@ import Rotate from "../../operations/Rotate.js"; import SeqUtils from "../../operations/SeqUtils.js"; import StrUtils from "../../operations/StrUtils.js"; import Tidy from "../../operations/Tidy.js"; +import ToTable from "../../operations/ToTable.js"; import Unicode from "../../operations/Unicode.js"; import UUID from "../../operations/UUID.js"; import XKCD from "../../operations/XKCD.js"; @@ -163,6 +164,7 @@ OpModules.Default = { "Mean": Arithmetic.runMean, "Median": Arithmetic.runMedian, "Standard Deviation": Arithmetic.runStdDev, + "To Table": ToTable.runToTable, "Windows Filetime to UNIX Timestamp": Filetime.runFromFiletimeToUnix, "UNIX Timestamp to Windows Filetime": Filetime.runToFiletimeFromUnix, "XKCD Random Number": XKCD.runRandomNumber, diff --git a/src/core/operations/ToTable.js b/src/core/operations/ToTable.js new file mode 100755 index 00000000..34227192 --- /dev/null +++ b/src/core/operations/ToTable.js @@ -0,0 +1,189 @@ +import Utils from "../Utils.js"; + +/** + * ToTable operations. + * + * @author Mark Jones [github.com/justanothermark] + * @namespace + */ + const ToTable = { + /** + * @constant + * @default + */ + SEPARATORS: [ + {name: "Comma", value:","}, + {name: "Tab", value: escape("\t")}, + {name: "Pipe", value: "|"}, + {name: "Custom", value: ""} + ], + + /** + * @constant + * @default + */ + FORMATS: [ + 'ASCII', + 'HTML' + ], + + /** + * To Table operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runToTable: function (input, args) { + let separator = args[1]; + let firstRowHeader = args[2]; + let format = args[3]; + let tableData = []; + + // If the separator contains any tabs, convert them to tab characters. + separator = separator.replace('\\t', '\t'); + + // Process the input into a nested array of elements. + let rows = input.split('\n'); + rows.forEach(function(element) { + if (separator == '') { + tableData.push([element]); + } + else { + tableData.push(element.split(separator)); + } + }); + + // Render the data in the requested format. + let output = ''; + switch (format) { + case 'ASCII': + output = asciiOutput(tableData); + break; + + default: + output = htmlOutput(tableData); + break; + } + + return output; + + /** + * Outputs an array of data as an ASCII table. + * + * @param {Array[]} tableData + * @returns {string} + */ + function asciiOutput(tableData) { + const horizontalBorder = '-'; + const verticalBorder = '|'; + const crossBorder = '+'; + + let output = ''; + let longestCells = []; + + // Find longestCells value per column to pad cells equally. + tableData.forEach(function(row, index) { + row.forEach(function(cell, cellIndex) { + if (longestCells[cellIndex] == undefined || cell.length > longestCells[cellIndex]) { + longestCells[cellIndex] = cell.length; + } + }); + }); + + // Calculate the complete row length. This is the length of the + // longest cell for each column plus 3 characters per cell + // (1 padding each side of the value and 1 for the cell border) + // plus 1 for the final cell border. + let rowLength = (longestCells.length * 3) + 1; + longestCells.forEach(function(celllongestCells) { + rowLength += celllongestCells; + }); + + // Add the top border of the table to the output. + output += outputHorizontalBorder(longestCells); + + // If the first row is a header, remove the row from the data and + // add it to the output with another horizontal border. + if (firstRowHeader) { + let row = tableData.shift(); + output += outputRow(row, longestCells); + output += outputHorizontalBorder(longestCells); + } + + // Add the rest of the table rows. + tableData.forEach(function(row, index) { + output += outputRow(row, longestCells); + }); + + // Close the table with a final horizontal border. + output += outputHorizontalBorder(longestCells); + + return output; + + /** + * Outputs a row of correctly padded cells. + */ + function outputRow(row, longestCells) { + let rowOutput = verticalBorder; + row.forEach(function(cell, index) { + rowOutput += ' ' + cell + ' '.repeat(longestCells[index] - cell.length) + ' ' + verticalBorder; + }); + rowOutput += '\n'; + return rowOutput; + } + + /** + * Outputs a horizontal border with a different character where + * the horizontal border meets a vertical border. + */ + function outputHorizontalBorder(longestCells) { + let rowOutput = crossBorder; + longestCells.forEach(function(cellLength) { + rowOutput += horizontalBorder.repeat(cellLength + 2) + crossBorder; + }); + rowOutput += '\n'; + return rowOutput; + } + } + + /** + * Outputs a table of data as a HTML table. + */ + function htmlOutput(tableData) { + // Start the HTML output with suitable classes for styling. + let output = ""; + + // If the first row is a header then put it in with "; + output += outputRow(row, 'th'); + output += ""; + } + + // Output the rest of the rows in the . + output += ""; + tableData.forEach(function(row, index) { + output += outputRow(row, 'td'); + }); + + // Close the body and table elements. + output += "
cells. + if (firstRowHeader) { + let row = tableData.shift(); + output += "
"; + return output; + + function outputRow(row, cellType) { + let output = ""; + row.forEach(function(cell) { + output += "<" + cellType + ">" + cell + ""; + }); + output += ""; + return output; + } + } + + return output; + } +}; + +export default ToTable; \ No newline at end of file From 411bba53a821257df9af528b7d2428370b6afc6f Mon Sep 17 00:00:00 2001 From: Mark Jones Date: Thu, 26 Apr 2018 13:00:35 +0100 Subject: [PATCH 2/3] Fix code style issues raised by linting. --- src/core/operations/ToTable.js | 64 +++++++++++++++------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/src/core/operations/ToTable.js b/src/core/operations/ToTable.js index 34227192..83887f26 100755 --- a/src/core/operations/ToTable.js +++ b/src/core/operations/ToTable.js @@ -1,19 +1,17 @@ -import Utils from "../Utils.js"; - /** * ToTable operations. * * @author Mark Jones [github.com/justanothermark] * @namespace */ - const ToTable = { +const ToTable = { /** * @constant * @default */ SEPARATORS: [ - {name: "Comma", value:","}, - {name: "Tab", value: escape("\t")}, + {name: "Comma", value: ","}, + {name: "Tab", value: "\\t"}, {name: "Pipe", value: "|"}, {name: "Custom", value: ""} ], @@ -23,8 +21,8 @@ import Utils from "../Utils.js"; * @default */ FORMATS: [ - 'ASCII', - 'HTML' + "ASCII", + "HTML" ], /** @@ -41,23 +39,22 @@ import Utils from "../Utils.js"; let tableData = []; // If the separator contains any tabs, convert them to tab characters. - separator = separator.replace('\\t', '\t'); + separator = separator.replace("\\t", "\t"); // Process the input into a nested array of elements. - let rows = input.split('\n'); + let rows = input.split("\n"); rows.forEach(function(element) { - if (separator == '') { + if (separator === "") { tableData.push([element]); - } - else { + } else { tableData.push(element.split(separator)); } }); // Render the data in the requested format. - let output = ''; + let output = ""; switch (format) { - case 'ASCII': + case "ASCII": output = asciiOutput(tableData); break; @@ -75,31 +72,22 @@ import Utils from "../Utils.js"; * @returns {string} */ function asciiOutput(tableData) { - const horizontalBorder = '-'; - const verticalBorder = '|'; - const crossBorder = '+'; + const horizontalBorder = "-"; + const verticalBorder = "|"; + const crossBorder = "+"; - let output = ''; + let output = ""; let longestCells = []; // Find longestCells value per column to pad cells equally. tableData.forEach(function(row, index) { row.forEach(function(cell, cellIndex) { - if (longestCells[cellIndex] == undefined || cell.length > longestCells[cellIndex]) { + if (longestCells[cellIndex] === undefined || cell.length > longestCells[cellIndex]) { longestCells[cellIndex] = cell.length; } }); }); - // Calculate the complete row length. This is the length of the - // longest cell for each column plus 3 characters per cell - // (1 padding each side of the value and 1 for the cell border) - // plus 1 for the final cell border. - let rowLength = (longestCells.length * 3) + 1; - longestCells.forEach(function(celllongestCells) { - rowLength += celllongestCells; - }); - // Add the top border of the table to the output. output += outputHorizontalBorder(longestCells); @@ -127,9 +115,9 @@ import Utils from "../Utils.js"; function outputRow(row, longestCells) { let rowOutput = verticalBorder; row.forEach(function(cell, index) { - rowOutput += ' ' + cell + ' '.repeat(longestCells[index] - cell.length) + ' ' + verticalBorder; + rowOutput += " " + cell + " ".repeat(longestCells[index] - cell.length) + " " + verticalBorder; }); - rowOutput += '\n'; + rowOutput += "\n"; return rowOutput; } @@ -142,7 +130,7 @@ import Utils from "../Utils.js"; longestCells.forEach(function(cellLength) { rowOutput += horizontalBorder.repeat(cellLength + 2) + crossBorder; }); - rowOutput += '\n'; + rowOutput += "\n"; return rowOutput; } } @@ -158,20 +146,26 @@ import Utils from "../Utils.js"; if (firstRowHeader) { let row = tableData.shift(); output += ""; - output += outputRow(row, 'th'); + output += outputRow(row, "th"); output += ""; } // Output the rest of the rows in the . output += ""; tableData.forEach(function(row, index) { - output += outputRow(row, 'td'); + output += outputRow(row, "td"); }); // Close the body and table elements. output += ""; return output; + /** + * Outputs a table row. + * + * @param {string[]} row + * @param {string} cellType + */ function outputRow(row, cellType) { let output = ""; row.forEach(function(cell) { @@ -181,9 +175,7 @@ import Utils from "../Utils.js"; return output; } } - - return output; } }; -export default ToTable; \ No newline at end of file +export default ToTable; From 8556bdcdeb2ef07029485fea239ec5ba0768a15d Mon Sep 17 00:00:00 2001 From: n1474335 Date: Fri, 4 May 2018 16:10:22 +0000 Subject: [PATCH 3/3] Tidied up 'To Table' operation, adding better CSV parsing support. --- src/core/Utils.js | 26 +++++++++------ src/core/config/Categories.js | 2 +- src/core/config/OperationConfig.js | 22 ++++++------- src/core/operations/ToTable.js | 51 ++++++++++-------------------- 4 files changed, 43 insertions(+), 58 deletions(-) diff --git a/src/core/Utils.js b/src/core/Utils.js index 16f581b3..4b20208b 100755 --- a/src/core/Utils.js +++ b/src/core/Utils.js @@ -737,37 +737,43 @@ const Utils = { * Parses CSV data and returns it as a two dimensional array or strings. * * @param {string} data + * @param {string[]} [cellDelims=[","]] + * @param {string[]} [lineDelims=["\n", "\r"]] * @returns {string[][]} * * @example * // returns [["head1", "head2"], ["data1", "data2"]] * Utils.parseCSV("head1,head2\ndata1,data2"); */ - parseCSV: function(data) { - + parseCSV: function(data, cellDelims=[","], lineDelims=["\n", "\r"]) { let b, - ignoreNext = false, + next, + renderNext = false, inString = false, cell = "", line = [], lines = []; + // Remove BOM, often present in Excel CSV files + if (data.length && data[0] === "\uFEFF") data = data.substr(1); + for (let i = 0; i < data.length; i++) { b = data[i]; - if (ignoreNext) { + next = data[i+1] || ""; + if (renderNext) { cell += b; - ignoreNext = false; + renderNext = false; } else if (b === "\\") { - cell += b; - ignoreNext = true; + renderNext = true; } else if (b === "\"" && !inString) { inString = true; } else if (b === "\"" && inString) { - inString = false; - } else if (b === "," && !inString) { + if (next === "\"") renderNext = true; + else inString = false; + } else if (!inString && cellDelims.indexOf(b) >= 0) { line.push(cell); cell = ""; - } else if ((b === "\n" || b === "\r") && !inString) { + } else if (!inString && lineDelims.indexOf(b) >= 0) { line.push(cell); cell = ""; lines.push(line); diff --git a/src/core/config/Categories.js b/src/core/config/Categories.js index 33ec4a99..9d531f8d 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -67,7 +67,6 @@ const Categories = [ "Encode text", "Decode text", "Swap endianness", - "To Table", ] }, { @@ -183,6 +182,7 @@ const Categories = [ "To Lower case", "Add line numbers", "Remove line numbers", + "To Table", "Reverse", "Sort", "Unique", diff --git a/src/core/config/OperationConfig.js b/src/core/config/OperationConfig.js index d3b8c27c..500a8803 100644 --- a/src/core/config/OperationConfig.js +++ b/src/core/config/OperationConfig.js @@ -616,26 +616,22 @@ const OperationConfig = { }, "To Table": { module: "Default", - description: "Renders data as a table. Data can be split on different characters and output as a HTML or ASCII table with optional header row.", + description: "Data can be split on different characters and rendered as an HTML or ASCII table with an optional header row.

Supports the CSV (Comma Separated Values) file format by default. Change the cell delimiter argument to \\t to support TSV (Tab Separated Values) or | for PSV (Pipe Separated Values).

You can enter as many delimiters as you like. Each character will be treat as a separate possible delimiter.", inputType: "string", outputType: "html", - highlight: false, - highlightReverse: false, - manualBake: false, args: [ { - name: "Select separator", - type: "populateOption", - value: ToTable.SEPARATORS, - target: 1 - }, - { - name: "Separator", - type: "string", + name: "Cell delimiters", + type: "binaryShortString", value: "," }, { - name: "First row header?", + name: "Row delimiters", + type: "binaryShortString", + value: "\\n\\r" + }, + { + name: "Make first row header", type: "boolean", value: false }, diff --git a/src/core/operations/ToTable.js b/src/core/operations/ToTable.js index 83887f26..b18b56f1 100755 --- a/src/core/operations/ToTable.js +++ b/src/core/operations/ToTable.js @@ -2,19 +2,14 @@ * ToTable operations. * * @author Mark Jones [github.com/justanothermark] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + * * @namespace */ +import Utils from "../Utils.js"; + const ToTable = { - /** - * @constant - * @default - */ - SEPARATORS: [ - {name: "Comma", value: ","}, - {name: "Tab", value: "\\t"}, - {name: "Pipe", value: "|"}, - {name: "Custom", value: ""} - ], /** * @constant @@ -25,6 +20,7 @@ const ToTable = { "HTML" ], + /** * To Table operation. * @@ -33,42 +29,26 @@ const ToTable = { * @returns {html} */ runToTable: function (input, args) { - let separator = args[1]; - let firstRowHeader = args[2]; - let format = args[3]; - let tableData = []; - - // If the separator contains any tabs, convert them to tab characters. - separator = separator.replace("\\t", "\t"); + const [cellDelims, rowDelims, firstRowHeader, format] = args; // Process the input into a nested array of elements. - let rows = input.split("\n"); - rows.forEach(function(element) { - if (separator === "") { - tableData.push([element]); - } else { - tableData.push(element.split(separator)); - } - }); + const tableData = Utils.parseCSV(input, cellDelims.split(""), rowDelims.split("")); + + if (!tableData.length) return ""; // Render the data in the requested format. - let output = ""; switch (format) { case "ASCII": - output = asciiOutput(tableData); - break; - + return asciiOutput(tableData); + case "HTML": default: - output = htmlOutput(tableData); - break; + return htmlOutput(tableData); } - return output; - /** * Outputs an array of data as an ASCII table. * - * @param {Array[]} tableData + * @param {string[][]} tableData * @returns {string} */ function asciiOutput(tableData) { @@ -137,6 +117,9 @@ const ToTable = { /** * Outputs a table of data as a HTML table. + * + * @param {string[][]} tableData + * @returns {string} */ function htmlOutput(tableData) { // Start the HTML output with suitable classes for styling.