diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index ce2f01f5..a18aabd0 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -3,6 +3,17 @@ "name": "Favourites", "ops": [] }, + { + "name": "Gaijin", + "ops": [ + "ByteAnalyser", + "Output", + "Pad", + "Prepend / Append", + "Store / Restore Input", + "Trim" + ] + }, { "name": "Data format", "ops": [ diff --git a/src/core/operations/gaijinatByteAnalyser.mjs b/src/core/operations/gaijinatByteAnalyser.mjs new file mode 100644 index 00000000..59e19599 --- /dev/null +++ b/src/core/operations/gaijinatByteAnalyser.mjs @@ -0,0 +1,208 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * ByteAnalyser operation + */ +class ByteAnalyser extends Operation { + + /** + * ByteAnalyser constructor + */ + constructor() { + super(); + + this.name = "ByteAnalyser"; + this.module = "Default"; + this.description = "Analyses the bytes in the input and displays statistics about them.

The histogram shows the distribution of the individual bytes."; + this.infoURL = ""; + this.inputType = "ArrayBuffer"; + this.outputType = "JSON"; + this.presentType = "html"; + this.args = [ + { + "name": "Show 0%s", + "type": "boolean", + "value": false + }, + { + "name": "Sort by count", + "type": "boolean", + "value": true + } + ]; + } + + /** + * @param {ArrayBuffer} input + * @param {Object[]} args + * @returns {JSON} + */ + run(input, args) { + const data = new Uint8Array(input); + const byteTable = [], + byteCount = data.length; + + let byteRepresented = 0, + byteRepresentedPercent = 0.0, + byteNotRepresented = 256, + byteNotRepresentedPercent = 0.0, + asciiCharsRepresented = "", + maxCount = 0, + i; + + // Initialisation of byte information data + for (i = 0; i < 256; i++) { + byteTable[i] = { + dec: i, + char: this.printableChar(i), + hex: i.toString(16).toUpperCase().padStart(2, "0"), + bin: i.toString(2).padStart(8, "0"), + count: 0, + percent: 0.0, + proportion: 0.0, + }; + } + + // Count bytes in data + for (i = 0; i < byteCount; ++i) { + byteTable[data[i]].count++; + } + + // Get the count of the most represented byte (100% in statistics) + for (i = 0; i < byteTable.length; ++i) { + if (byteTable[i].count > maxCount) maxCount = byteTable[i].count; + } + + // Calculate percentage occurrence of bytes + const divPercent = (100 / byteCount); + const divProportion = (100 / maxCount); + for (i = 0; i < byteTable.length; ++i) { + byteTable[i].percent = (byteTable[i].count * divPercent).toFixed(4); + byteTable[i].proportion = (byteTable[i].count * divProportion).toFixed(2); + + if (byteTable[i].count > 0) { + // Collect number of represented bytes + byteRepresented++; + // Collect represented printable ASCII characters + if ((i >= 32) && (i <= 127)) { + asciiCharsRepresented += byteTable[i].char; + } + } + } + + byteRepresentedPercent = (byteRepresented * (100 / 256)).toFixed(2); + byteNotRepresented = 256 - byteRepresented; + byteNotRepresentedPercent = (byteNotRepresented * (100 / 256)).toFixed(2); + + // console.log(byteTable); + + return { + "byteTable": byteTable, + "byteCount": byteCount, + "byteRepresented": byteRepresented, + "byteRepresentedPercent": byteRepresentedPercent, + "byteNotRepresented": byteNotRepresented, + "byteNotRepresentedPercent": byteNotRepresentedPercent, + "asciiCharsRepresented": asciiCharsRepresented, + }; + } + + /** + * Displays the statistics for web apps. + * + * @param {json} statistics + * @returns {html} + */ + present(stat, args) { + const [showZeros, sortCount] = args; + + let output = "", + i; + + const clsc = ' class="text-center" style="background-color: var(--secondary-background-colour);"', + clsr = ' class="text-right"'; + + // Histogram + output += '
'; + output += ''; + for (i = 0; i < 256; i++) { + output += '`; + } + output += "
\n"; + + // Overview + output += ''; + output += `Number of bytes total:`; + output += `Number of bytes represented:${stat.byteRepresented.toLocaleString("en")}(${stat.byteRepresentedPercent.toLocaleString("en")}%)`; + output += `Number of bytes not represented:${stat.byteNotRepresented.toLocaleString("en")}(${stat.byteNotRepresentedPercent.toLocaleString("en")}%)`; + output += "
${stat.byteCount.toLocaleString("en")}
\n"; + + output += "

Represented printable ASCII characters:
" + this.htmlEntities(stat.asciiCharsRepresented) + "

\n"; + + // Details + if (sortCount) { + stat.byteTable.sort(this.compareByteTableItemsCount); + } + output += ''; + output += `BinaryHexCodeCharCountPercent`; + for (i = 0; i < 256; i++) { + if (!showZeros && (stat.byteTable[i].count === 0)) continue; + output += `${stat.byteTable[i].bin}${stat.byteTable[i].hex}`; + output += `${stat.byteTable[i].dec.toString()}${this.htmlEntities(stat.byteTable[i].char)}`; + output += `${stat.byteTable[i].count.toLocaleString("en")}${stat.byteTable[i].percent.toLocaleString("en")}`; + } + output += "
\n"; + + return output; + } + + + /** + * Gets the printable character for a byte. + * + * @param {UInt8} charCode + * @returns {html} + */ + printableChar(charCode) { + if (charCode < 32) return " "; + if (charCode === 127) return " "; + + return String.fromCharCode(charCode); + } + + /** + * Compare byteTable items by count. + */ + compareByteTableItemsCount(a, b) { + if (a.count > b.count) { + return -1; + } else if (a.count < b.count) { + return 1; + } else { + return 0; + } + } + + /** + * Converts reserved characters to HTML entities. + * + * @param {string} string + * @returns {string} + */ + htmlEntities(string) { + return String(string).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + +} + +export default ByteAnalyser; diff --git a/src/core/operations/gaijinatOutput.mjs b/src/core/operations/gaijinatOutput.mjs new file mode 100644 index 00000000..ce5cd8b7 --- /dev/null +++ b/src/core/operations/gaijinatOutput.mjs @@ -0,0 +1,53 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; + +/** + * Output operation + */ +class Output extends Operation { + + /** + * Output constructor + */ + constructor() { + super(); + + this.name = "Output"; + this.module = "Default"; + this.description = "Outputs the entered text.

This is useful to output text with stored registers.

Example:
Assuming $R0 is test, the string:
Value=$R0
will output:
Value=test"; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Output", + type: "toggleString", + value: "", + toggleValues: ["Simple string", "Extended (\\n, \\t, \\x...)"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const typeOutput = args[0].option; + let strOutput = args[0].string; + + if (typeOutput.startsWith("Extended")) strOutput = Utils.parseEscapedChars(strOutput); + + return strOutput; + } + +} + +export default Output; diff --git a/src/core/operations/gaijinatPad.mjs b/src/core/operations/gaijinatPad.mjs new file mode 100644 index 00000000..b2d7a4ba --- /dev/null +++ b/src/core/operations/gaijinatPad.mjs @@ -0,0 +1,113 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; + +/** + * Pad operation + */ +class Pad extends Operation { + + /** + * Pad constructor + */ + constructor() { + super(); + + this.name = "Pad"; + this.module = "Default"; + this.description = "Fills the input with one or more characters until the specified length is reached."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Position", + type: "option", + value: ["Start", "End", "Both"] + }, + { + name: "Length", + type: "number", + value: 0 + }, + { + name: "String", + type: "toggleString", + value: "", + toggleValues: ["Simple string", "Extended (\\n, \\t, \\x...)"] + }, + { + name: "Apply to", + type: "option", + value: ["Input", "Lines"], + defaultIndex: 1 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [position, padLength, {option: type}, applyTo] = args; + let padCharacters = args[2].string; + let output = ""; + let joinCharLenght = 0; + + if (type.startsWith("Extended")) padCharacters = Utils.parseEscapedChars(padCharacters); + if (padCharacters === "") padCharacters = " "; + + if (applyTo === "Lines") { + input = input.split("\n"); + joinCharLenght = 1; + for (let i = 0; i < input.length; i++) { + output += this.padEx(input[i], position, padLength, padCharacters) + "\n"; + } + } else { + output = this.padEx(input, position, padLength, padCharacters); + } + + if (output === "") { + return input; + } else { + if (joinCharLenght > 0) output = output.slice(0, output.length - joinCharLenght); + return output; + } + } + + /** + * @param {string} input + * @param {integer} position + * @param {integer} padLength + * @param {string} padCharacters + * @returns {string} + */ + padEx(input, position, padLength, padCharacters) { + let spaceLength, padStartLength; + let output = ""; + + if (position === "Start") { + output = input.padStart(padLength, padCharacters); + } else if (position === "End") { + output = input.padEnd(padLength, padCharacters); + } else if (position === "Both") { + spaceLength = padLength - input.length; + if (spaceLength > 0) { + padStartLength = Math.floor(spaceLength / 2) + input.length; + output = input.padStart(padStartLength, padCharacters).padEnd(padLength, padCharacters); + } + } + + return output; + } + +} + +export default Pad; diff --git a/src/core/operations/gaijinatPrependAppend.mjs b/src/core/operations/gaijinatPrependAppend.mjs new file mode 100644 index 00000000..8478edd2 --- /dev/null +++ b/src/core/operations/gaijinatPrependAppend.mjs @@ -0,0 +1,81 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; + +/** + * Prepend / Append operation + */ +class PrependAppend extends Operation { + + /** + * PrependAppend constructor + */ + constructor() { + super(); + + this.name = "Prepend / Append"; + this.module = "Default"; + this.description = "Adds the specified text to the beginning and/or end of each line, character or the entire input.

Includes support for simple strings and extended strings (which support \\n, \\r, \\t, \\b, \\f and escaped hex bytes using \\x notation, e.g. \\x00 for a null byte)."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Prepend", + type: "toggleString", + value: "", + toggleValues: ["Simple string", "Extended (\\n, \\t, \\x...)"] + }, + { + name: "Append", + type: "toggleString", + value: "", + toggleValues: ["Simple string", "Extended (\\n, \\t, \\x...)"] + }, + { + name: "Apply to", + type: "option", + value: ["Input", "Lines", "Characters"], + defaultIndex: 1 + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [{option: typePrepend}, {option: typeAppend}, applyTo] = args; + let [{string: strPrepend}, {string: strAppend}] = args; + let output = ""; + let joinChar = ""; + + if (typePrepend.startsWith("Extended")) strPrepend = Utils.parseEscapedChars(strPrepend); + if (typeAppend.startsWith("Extended")) strAppend = Utils.parseEscapedChars(strAppend); + + if (applyTo === "Input") { + output = strPrepend + input + strAppend; + } else { + if (applyTo === "Lines") { + input = input.split("\n"); + joinChar = "\n"; + } + for (let i = 0; i < input.length; i++) { + output += strPrepend + input[i] + strAppend + joinChar; + } + } + + if (joinChar.length > 0) output = output.slice(0, output.length - joinChar.length); + return output; + } + +} + +export default PrependAppend; diff --git a/src/core/operations/gaijinatStoreRestoreInput.mjs b/src/core/operations/gaijinatStoreRestoreInput.mjs new file mode 100644 index 00000000..372d960c --- /dev/null +++ b/src/core/operations/gaijinatStoreRestoreInput.mjs @@ -0,0 +1,99 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Dish from "../Dish.mjs"; + +/** + * StoreRestoreInput operation + */ +class StoreRestoreInput extends Operation { + + static variables = {}; + + /** + * StoreRestoreInput constructor + */ + constructor() { + super(); + + this.name = "Store / Restore Input"; + this.flowControl = true; + this.module = "Default"; + this.description = "Stores the input value and restores it later as output.

Store stores the input under the given name.
Restore restores the input with the given name as output.
Clear removes the stored input with the given name. Without a name, all stored inputs will be removed.

You should deactivate 'Auto Bake' for this operation."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Mode", + type: "option", + value: ["Clear", "Store", "Restore"] + }, + { + name: "Name", + type: "string", + value: "" + }, + ]; + } + + /** + * @param {Object} state - The current state of the recipe. + * @param {number} state.progress - The current position in the recipe. + * @param {Dish} state.dish - The Dish being operated on. + * @param {Operation[]} state.opList - The list of operations in the recipe. + * @returns {Object} The updated state of the recipe. + */ + async run(state) { + const ings = state.opList[state.progress].ingValues; + const [mode, varName] = ings; + const input = await state.dish.get(Dish.STRING); + + let firstOpIndex = -1; + + // Find the first index that matches this operation and clear the variables + for (let i = 0; i < state.opList.length; i++) { + if (state.opList[i].name === this.name) { + firstOpIndex = i; + break; + } + } + if (state.progress === firstOpIndex) { + StoreRestoreInput.variables = {}; + } + + if (mode === "Clear") { + + if (varName === "") { + StoreRestoreInput.variables = {}; + } else { + if (StoreRestoreInput.variables[varName] !== undefined) { + delete StoreRestoreInput.variables[varName]; + } + } + + } else if (varName && (mode === "Store")) { + + StoreRestoreInput.variables[varName] = input; + + } else if (varName && (mode === "Restore")) { + + if (StoreRestoreInput.variables[varName] !== undefined) { + state.dish.set(StoreRestoreInput.variables[varName], Dish.STRING); + return state; + } + + } + + // console.log(StoreRestoreInput.variables); + + return state; + } + +} + +export default StoreRestoreInput; diff --git a/src/core/operations/gaijinatTrim.mjs b/src/core/operations/gaijinatTrim.mjs new file mode 100644 index 00000000..59706214 --- /dev/null +++ b/src/core/operations/gaijinatTrim.mjs @@ -0,0 +1,61 @@ +/** + * @author gaijinat [web@gaijin.at] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Trim operation + */ +class Trim extends Operation { + + /** + * Trim constructor + */ + constructor() { + super(); + + this.name = "Trim"; + this.module = "Default"; + this.description = "Removes all whitespaces and line breaks from the beginning, end or both of the input data."; + this.infoURL = ""; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Position", + type: "option", + value: ["Start", "End", "Both"], + defaultIndex: 2 + }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [position] = args; + let output = ""; + + switch (position) { + case "Start": + output = input.trimStart(); + break; + case "End": + output = input.trimEnd(); + break; + default: + output = input.trim(); + } + + return output; + } + +} + +export default Trim;