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 b2c404f6..9d531f8d 100755 --- a/src/core/config/Categories.js +++ b/src/core/config/Categories.js @@ -182,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 9c33d3d0..500a8803 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,34 @@ const OperationConfig = { } ] }, + "To Table": { + module: "Default", + 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", + args: [ + { + name: "Cell delimiters", + type: "binaryShortString", + value: "," + }, + { + name: "Row delimiters", + type: "binaryShortString", + value: "\\n\\r" + }, + { + name: "Make 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..b18b56f1 --- /dev/null +++ b/src/core/operations/ToTable.js @@ -0,0 +1,164 @@ +/** + * 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 + */ + FORMATS: [ + "ASCII", + "HTML" + ], + + + /** + * To Table operation. + * + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + runToTable: function (input, args) { + const [cellDelims, rowDelims, firstRowHeader, format] = args; + + // Process the input into a nested array of elements. + const tableData = Utils.parseCSV(input, cellDelims.split(""), rowDelims.split("")); + + if (!tableData.length) return ""; + + // Render the data in the requested format. + switch (format) { + case "ASCII": + return asciiOutput(tableData); + case "HTML": + default: + return htmlOutput(tableData); + } + + /** + * Outputs an array of data as an ASCII table. + * + * @param {string[][]} 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; + } + }); + }); + + // 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. + * + * @param {string[][]} tableData + * @returns {string} + */ + 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; + + /** + * Outputs a table row. + * + * @param {string[]} row + * @param {string} cellType + */ + function outputRow(row, cellType) { + let output = ""; + row.forEach(function(cell) { + output += "<" + cellType + ">" + cell + ""; + }); + output += ""; + return output; + } + } + } +}; + +export default ToTable;