diff --git a/.babelrc b/.babelrc index 92a857cf..094c0592 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,7 @@ "chrome": 40, "firefox": 35, "edge": 14, - "node": "6.5", + "node": "6.5" }, "modules": false, "useBuiltIns": true diff --git a/.eslintignore b/.eslintignore index 1034aa8a..83279ae8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1 @@ -src/core/lib/** -src/core/config/MetaConfig.js \ No newline at end of file +src/core/vendor/** \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index d63e35e8..5d0a6331 100755 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -89,7 +89,6 @@ "globals": { "$": false, "jQuery": false, - "moment": false, "log": false, "COMPILE_TIME": false, diff --git a/.gitignore b/.gitignore index 79c28325..31b15bd9 100755 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ build docs/* !docs/*.conf.json !docs/*.ico -.vscode -src/core/config/MetaConfig.js \ No newline at end of file +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0cb60d89..805f6b45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: - - "8.4" + - node install: npm install before_script: - npm install -g grunt diff --git a/Gruntfile.js b/Gruntfile.js index 7a9890f1..4b0eef0e 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,7 +4,8 @@ const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const NodeExternals = require("webpack-node-externals"); const Inliner = require("web-resource-inliner"); -const fs = require("fs"); +const glob = require("glob"); +const path = require("path"); /** * Grunt configuration for building the app in various formats. @@ -21,15 +22,15 @@ module.exports = function (grunt) { // Tasks grunt.registerTask("dev", "A persistent task which creates a development build whenever source files are modified.", - ["clean:dev", "concurrent:dev"]); + ["clean:dev", "clean:config", "webpack-dev-server:start"]); grunt.registerTask("node", "Compiles CyberChef into a single NodeJS module.", - ["clean:node", "webpack:metaConf", "webpack:node", "chmod:build"]); + ["clean:node", "clean:config", "webpack:node", "chmod:build"]); grunt.registerTask("test", "A task which runs all the tests in test/tests.", - ["clean:test", "webpack:metaConf", "webpack:tests", "execute:test"]); + ["exec:tests"]); grunt.registerTask("docs", "Compiles documentation in the /docs directory.", @@ -37,7 +38,7 @@ module.exports = function (grunt) { grunt.registerTask("prod", "Creates a production-ready build. Use the --msg flag to add a compile message.", - ["eslint", "clean:prod", "webpack:metaConf", "webpack:web", "inline", "chmod"]); + ["eslint", "clean:prod", "clean:config", "webpack:web", "inline", "chmod"]); grunt.registerTask("default", "Lints the code base", @@ -62,9 +63,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks("grunt-contrib-copy"); grunt.loadNpmTasks("grunt-chmod"); grunt.loadNpmTasks("grunt-exec"); - grunt.loadNpmTasks("grunt-execute"); grunt.loadNpmTasks("grunt-accessibility"); - grunt.loadNpmTasks("grunt-concurrent"); // Project configuration @@ -118,12 +117,12 @@ module.exports = function (grunt) { * Generates an entry list for all the modules. */ function listEntryModules() { - const path = "./src/core/config/modules/"; let entryModules = {}; - fs.readdirSync(path).forEach(file => { - if (file !== "Default.js" && file !== "OpModules.js") - entryModules[file.split(".js")[0]] = path + file; + glob.sync("./src/core/config/modules/*.mjs").forEach(file => { + const basename = path.basename(file); + if (basename !== "Default.mjs" && basename !== "OpModules.mjs") + entryModules[basename.split(".mjs")[0]] = path.resolve(file); }); return entryModules; @@ -132,9 +131,9 @@ module.exports = function (grunt) { grunt.initConfig({ clean: { dev: ["build/dev/*"], - prod: ["build/prod/*", "src/core/config/MetaConfig.js"], - test: ["build/test/*", "src/core/config/MetaConfig.js"], - node: ["build/node/*", "src/core/config/MetaConfig.js"], + prod: ["build/prod/*"], + node: ["build/node/*"], + config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*"], docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"], inlineScripts: ["build/prod/scripts.js"], }, @@ -143,10 +142,10 @@ module.exports = function (grunt) { configFile: "./.eslintrc.json" }, configs: ["Gruntfile.js"], - core: ["src/core/**/*.js", "!src/core/lib/**/*", "!src/core/config/MetaConfig.js"], - web: ["src/web/**/*.js"], - node: ["src/node/**/*.js"], - tests: ["test/**/*.js"], + core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*"], + web: ["src/web/**/*.{js,mjs}"], + node: ["src/node/**/*.{js,mjs}"], + tests: ["test/**/*.{js,mjs}"], }, jsdoc: { options: { @@ -159,17 +158,11 @@ module.exports = function (grunt) { all: { src: [ "src/**/*.js", - "!src/core/lib/**/*", - "!src/core/config/MetaConfig.js" + "src/**/*.mjs", + "!src/core/vendor/**/*" ], } }, - concurrent: { - options: { - logConcurrentOutput: true - }, - dev: ["webpack:metaConfDev", "webpack-dev-server:start"] - }, accessibility: { options: { accessibilityLevel: "WCAG2A", @@ -184,39 +177,6 @@ module.exports = function (grunt) { }, webpack: { options: webpackConfig, - metaConf: { - mode: "production", - target: "node", - entry: [ - "babel-polyfill", - "./src/core/config/OperationConfig.js" - ], - output: { - filename: "MetaConfig.js", - path: __dirname + "/src/core/config/", - library: "MetaConfig", - libraryTarget: "commonjs2", - libraryExport: "default" - }, - externals: [NodeExternals()], - }, - metaConfDev: { - mode: "development", - target: "node", - entry: [ - "babel-polyfill", - "./src/core/config/OperationConfig.js" - ], - output: { - filename: "MetaConfig.js", - path: __dirname + "/src/core/config/", - library: "MetaConfig", - libraryTarget: "commonjs2", - libraryExport: "default" - }, - externals: [NodeExternals()], - watch: true - }, web: { mode: "production", target: "web", @@ -229,7 +189,7 @@ module.exports = function (grunt) { }, resolve: { alias: { - "./config/modules/OpModules.js": "./config/modules/Default.js" + "./config/modules/OpModules": "./config/modules/Default" } }, plugins: [ @@ -279,7 +239,7 @@ module.exports = function (grunt) { tests: { mode: "development", target: "node", - entry: "./test/index.js", + entry: "./test/index.mjs", externals: [NodeExternals()], output: { filename: "index.js", @@ -292,7 +252,7 @@ module.exports = function (grunt) { node: { mode: "production", target: "node", - entry: "./src/node/index.js", + entry: "./src/node/index.mjs", externals: [NodeExternals()], output: { filename: "CyberChef.js", @@ -330,7 +290,7 @@ module.exports = function (grunt) { }, moduleEntryPoints), resolve: { alias: { - "./config/modules/OpModules.js": "./config/modules/Default.js" + "./config/modules/OpModules": "./config/modules/Default" } }, plugins: [ @@ -401,10 +361,10 @@ module.exports = function (grunt) { }, sitemap: { command: "node build/prod/sitemap.js > build/prod/sitemap.xml" + }, + tests: { + command: "node --experimental-modules test/index.mjs" } }, - execute: { - test: "build/test/index.js" - }, }); }; diff --git a/package-lock.json b/package-lock.json index 8a220bc7..0354d97d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5300,26 +5300,6 @@ "shelljs": "0.5.3" } }, - "grunt-concurrent": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/grunt-concurrent/-/grunt-concurrent-2.3.1.tgz", - "integrity": "sha1-Hj2zjM71o9oRleYdYx/n4yE0TSM=", - "dev": true, - "requires": { - "arrify": "1.0.1", - "async": "1.5.2", - "indent-string": "2.1.0", - "pad-stream": "1.2.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - } - } - }, "grunt-contrib-clean": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.1.0.tgz", @@ -5460,12 +5440,6 @@ "integrity": "sha512-cgAlreXf3muSYS5LzW0Cc4xHK03BjFOYk0MqCQ/MZ3k1Xz2GU7D+IAJg4UKicxpO+XdONJdx/NJ6kpy2wI+uHg==", "dev": true }, - "grunt-execute": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz", - "integrity": "sha1-TpRf5XlZzA3neZCDtrQq7ZYWNQo=", - "dev": true - }, "grunt-jsdoc": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/grunt-jsdoc/-/grunt-jsdoc-2.2.1.tgz", @@ -8419,19 +8393,6 @@ "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", "dev": true }, - "pad-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pad-stream/-/pad-stream-1.2.0.tgz", - "integrity": "sha1-Yx3Mn3mBC3BZZeid7eps/w/B38k=", - "dev": true, - "requires": { - "meow": "3.7.0", - "pumpify": "1.3.5", - "repeating": "2.0.1", - "split2": "1.1.1", - "through2": "2.0.3" - } - }, "pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", @@ -11046,15 +11007,6 @@ "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.3.5.tgz", "integrity": "sha1-YuLOZtLPkcx3SqXwdJ/yUTgDn1A=" }, - "split2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/split2/-/split2-1.1.1.tgz", - "integrity": "sha1-Fi2bGIZfAqsvKtlYVSLbm1TEgfk=", - "dev": true, - "requires": { - "through2": "2.0.3" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -12198,15 +12150,6 @@ "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", "dev": true }, - "val-loader": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/val-loader/-/val-loader-1.1.0.tgz", - "integrity": "sha512-8m62XF42FcfrBBl02rtDY9hQhDcDczrEcr60/aSMxlzJiXAcbAimRPvsDoDa5QcGAusOgOmVTpFtK5EbfZdDwA==", - "dev": true, - "requires": { - "loader-utils": "1.1.0" - } - }, "valid-data-url": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.4.tgz", @@ -12709,6 +12652,12 @@ "integrity": "sha1-Iyxi7GCSsQBjWj0p2DwXRxKN+b0=", "dev": true }, + "webpack-shell-plugin": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-shell-plugin/-/webpack-shell-plugin-0.5.0.tgz", + "integrity": "sha1-Kbih2A3erg3bEOcpZn9yhlPCx0I=", + "dev": true + }, "webpack-sources": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", diff --git a/package.json b/package.json index 6d9e75b2..119d915b 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,10 @@ "grunt": ">=1.0.2", "grunt-accessibility": "~6.0.0", "grunt-chmod": "~1.1.1", - "grunt-concurrent": "^2.3.1", "grunt-contrib-clean": "~1.1.0", "grunt-contrib-copy": "~1.0.0", "grunt-eslint": "^20.1.0", "grunt-exec": "~3.0.0", - "grunt-execute": "^0.2.2", "grunt-jsdoc": "^2.2.1", "grunt-webpack": "^3.0.2", "html-webpack-plugin": "^3.0.4", @@ -61,11 +59,11 @@ "sitemap": "^1.13.0", "style-loader": "^0.20.2", "url-loader": "^0.6.2", - "val-loader": "^1.1.0", "web-resource-inliner": "^4.2.1", "webpack": "^4.0.1", "webpack-dev-server": "^3.1.0", "webpack-node-externals": "^1.6.0", + "webpack-shell-plugin": "^0.5.0", "worker-loader": "^1.1.1" }, "dependencies": { diff --git a/src/core/Chef.js b/src/core/Chef.js deleted file mode 100755 index aba3f4f7..00000000 --- a/src/core/Chef.js +++ /dev/null @@ -1,165 +0,0 @@ -import Dish from "./Dish.js"; -import Recipe from "./Recipe.js"; - - -/** - * The main controller for CyberChef. - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @class - */ -const Chef = function() { - this.dish = new Dish(); -}; - - -/** - * Runs the recipe over the input. - * - * @param {string|ArrayBuffer} input - The input data as a string or ArrayBuffer - * @param {Object[]} recipeConfig - The recipe configuration object - * @param {Object} options - The options object storing various user choices - * @param {boolean} options.attempHighlight - Whether or not to attempt highlighting - * @param {number} progress - The position in the recipe to start from - * @param {number} [step] - Whether to only execute one operation in the recipe - * - * @returns {Object} response - * @returns {string} response.result - The output of the recipe - * @returns {string} response.type - The data type of the result - * @returns {number} response.progress - The position that we have got to in the recipe - * @returns {number} response.duration - The number of ms it took to execute the recipe - * @returns {number} response.error - The error object thrown by a failed operation (false if no error) -*/ -Chef.prototype.bake = async function(input, recipeConfig, options, progress, step) { - log.debug("Chef baking"); - const startTime = new Date().getTime(), - recipe = new Recipe(recipeConfig), - containsFc = recipe.containsFlowControl(), - notUTF8 = options && options.hasOwnProperty("treatAsUtf8") && !options.treatAsUtf8; - let error = false; - - if (containsFc && ENVIRONMENT_IS_WORKER()) self.setOption("attemptHighlight", false); - - // Clean up progress - if (progress >= recipeConfig.length) { - progress = 0; - } - - if (step) { - // Unset breakpoint on this step - recipe.setBreakpoint(progress, false); - // Set breakpoint on next step - recipe.setBreakpoint(progress + 1, true); - } - - // If stepping with flow control, we have to start from the beginning - // but still want to skip all previous breakpoints - if (progress > 0 && containsFc) { - recipe.removeBreaksUpTo(progress); - progress = 0; - } - - // If starting from scratch, load data - if (progress === 0) { - const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING; - this.dish.set(input, type); - } - - try { - progress = await recipe.execute(this.dish, progress); - } catch (err) { - log.error(err); - error = { - displayStr: err.displayStr, - }; - progress = err.progress; - } - - // Depending on the size of the output, we may send it back as a string or an ArrayBuffer. - // This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file. - // The threshold is specified in KiB. - const threshold = (options.ioDisplayThreshold || 1024) * 1024; - const returnType = this.dish.size() > threshold ? Dish.ARRAY_BUFFER : Dish.STRING; - - return { - result: this.dish.type === Dish.HTML ? - this.dish.get(Dish.HTML, notUTF8) : - this.dish.get(returnType, notUTF8), - type: Dish.enumLookup(this.dish.type), - progress: progress, - duration: new Date().getTime() - startTime, - error: error - }; -}; - - -/** - * When a browser tab is unfocused and the browser has to run lots of dynamic content in other tabs, - * it swaps out the memory for that tab. If the CyberChef tab has been unfocused for more than a - * minute, we run a silent bake which will force the browser to load and cache all the relevant - * JavaScript code needed to do a real bake. - * - * This will stop baking taking a long time when the CyberChef browser tab has been unfocused for a - * long time and the browser has swapped out all its memory. - * - * The output will not be modified (hence "silent" bake). - * - * This will only actually execute the recipe if auto-bake is enabled, otherwise it will just load - * the recipe, ingredients and dish. - * - * @param {Object[]} recipeConfig - The recipe configuration object - * @returns {number} The time it took to run the silent bake in milliseconds. -*/ -Chef.prototype.silentBake = function(recipeConfig) { - log.debug("Running silent bake"); - - let startTime = new Date().getTime(), - recipe = new Recipe(recipeConfig), - dish = new Dish("", Dish.STRING); - - try { - recipe.execute(dish); - } catch (err) { - // Suppress all errors - } - return new Date().getTime() - startTime; -}; - - -/** - * Calculates highlight offsets if possible. - * - * @param {Object[]} recipeConfig - * @param {string} direction - * @param {Object} pos - The position object for the highlight. - * @param {number} pos.start - The start offset. - * @param {number} pos.end - The end offset. - * @returns {Object} - */ -Chef.prototype.calculateHighlights = function(recipeConfig, direction, pos) { - const recipe = new Recipe(recipeConfig); - const highlights = recipe.generateHighlightList(); - - if (!highlights) return false; - - for (let i = 0; i < highlights.length; i++) { - // Remove multiple highlights before processing again - pos = [pos[0]]; - - const func = direction === "forward" ? highlights[i].f : highlights[i].b; - - if (typeof func == "function") { - pos = func(pos, highlights[i].args); - } - } - - return { - pos: pos, - direction: direction - }; -}; - -export default Chef; diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs new file mode 100755 index 00000000..c7338184 --- /dev/null +++ b/src/core/Chef.mjs @@ -0,0 +1,172 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Dish from "./Dish"; +import Recipe from "./Recipe"; +import log from "loglevel"; + +/** + * The main controller for CyberChef. + */ +class Chef { + + /** + * Chef constructor + */ + constructor() { + this.dish = new Dish(); + } + + + /** + * Runs the recipe over the input. + * + * @param {string|ArrayBuffer} input - The input data as a string or ArrayBuffer + * @param {Object[]} recipeConfig - The recipe configuration object + * @param {Object} options - The options object storing various user choices + * @param {boolean} options.attempHighlight - Whether or not to attempt highlighting + * @param {number} progress - The position in the recipe to start from + * @param {number} [step] - Whether to only execute one operation in the recipe + * + * @returns {Object} response + * @returns {string} response.result - The output of the recipe + * @returns {string} response.type - The data type of the result + * @returns {number} response.progress - The position that we have got to in the recipe + * @returns {number} response.duration - The number of ms it took to execute the recipe + * @returns {number} response.error - The error object thrown by a failed operation (false if no error) + */ + async bake(input, recipeConfig, options, progress, step) { + log.debug("Chef baking"); + const startTime = new Date().getTime(), + recipe = new Recipe(recipeConfig), + containsFc = recipe.containsFlowControl(), + notUTF8 = options && options.hasOwnProperty("treatAsUtf8") && !options.treatAsUtf8; + let error = false; + + if (containsFc && ENVIRONMENT_IS_WORKER()) self.setOption("attemptHighlight", false); + + // Clean up progress + if (progress >= recipeConfig.length) { + progress = 0; + } + + if (step) { + // Unset breakpoint on this step + recipe.setBreakpoint(progress, false); + // Set breakpoint on next step + recipe.setBreakpoint(progress + 1, true); + } + + // If stepping with flow control, we have to start from the beginning + // but still want to skip all previous breakpoints + if (progress > 0 && containsFc) { + recipe.removeBreaksUpTo(progress); + progress = 0; + } + + // If starting from scratch, load data + if (progress === 0) { + const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING; + this.dish.set(input, type); + } + + try { + progress = await recipe.execute(this.dish, progress); + } catch (err) { + log.error(err); + error = { + displayStr: err.displayStr, + }; + progress = err.progress; + } + + // Depending on the size of the output, we may send it back as a string or an ArrayBuffer. + // This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file. + // The threshold is specified in KiB. + const threshold = (options.ioDisplayThreshold || 1024) * 1024; + const returnType = this.dish.size > threshold ? Dish.ARRAY_BUFFER : Dish.STRING; + + return { + result: this.dish.type === Dish.HTML ? + this.dish.get(Dish.HTML, notUTF8) : + this.dish.get(returnType, notUTF8), + type: Dish.enumLookup(this.dish.type), + progress: progress, + duration: new Date().getTime() - startTime, + error: error + }; + } + + + /** + * When a browser tab is unfocused and the browser has to run lots of dynamic content in other tabs, + * it swaps out the memory for that tab. If the CyberChef tab has been unfocused for more than a + * minute, we run a silent bake which will force the browser to load and cache all the relevant + * JavaScript code needed to do a real bake. + * + * This will stop baking taking a long time when the CyberChef browser tab has been unfocused for a + * long time and the browser has swapped out all its memory. + * + * The output will not be modified (hence "silent" bake). + * + * This will only actually execute the recipe if auto-bake is enabled, otherwise it will just load + * the recipe, ingredients and dish. + * + * @param {Object[]} recipeConfig - The recipe configuration object + * @returns {number} The time it took to run the silent bake in milliseconds. + */ + silentBake(recipeConfig) { + log.debug("Running silent bake"); + + let startTime = new Date().getTime(), + recipe = new Recipe(recipeConfig), + dish = new Dish("", Dish.STRING); + + try { + recipe.execute(dish); + } catch (err) { + // Suppress all errors + } + return new Date().getTime() - startTime; + } + + + /** + * Calculates highlight offsets if possible. + * + * @param {Object[]} recipeConfig + * @param {string} direction + * @param {Object} pos - The position object for the highlight. + * @param {number} pos.start - The start offset. + * @param {number} pos.end - The end offset. + * @returns {Object} + */ + calculateHighlights(recipeConfig, direction, pos) { + const recipe = new Recipe(recipeConfig); + const highlights = recipe.generateHighlightList(); + + if (!highlights) return false; + + for (let i = 0; i < highlights.length; i++) { + // Remove multiple highlights before processing again + pos = [pos[0]]; + + const func = direction === "forward" ? highlights[i].f : highlights[i].b; + + if (typeof func == "function") { + pos = func(pos, highlights[i].args); + } + } + + return { + pos: pos, + direction: direction + }; + } + +} + +export default Chef; diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index a091e09c..3ef65a95 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -7,9 +7,9 @@ */ import "babel-polyfill"; -import Chef from "./Chef.js"; -import OperationConfig from "./config/MetaConfig.js"; -import OpModules from "./config/modules/Default.js"; +import Chef from "./Chef"; +import OperationConfig from "./config/OperationConfig.json"; +import OpModules from "./config/modules/Default"; // Add ">" to the start of all log messages in the Chef Worker import loglevelMessagePrefix from "loglevel-message-prefix"; diff --git a/src/core/Dish.js b/src/core/Dish.js deleted file mode 100755 index f0093e81..00000000 --- a/src/core/Dish.js +++ /dev/null @@ -1,274 +0,0 @@ -import Utils from "./Utils.js"; -import BigNumber from "bignumber.js"; - -/** - * The data being operated on by each operation. - * - * @author n1474335 [n1474335@gmail.com] - * @author Matt C [matt@artemisbot.uk] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @class - * @param {byteArray|string|number|ArrayBuffer|BigNumber} value - The value of the input data. - * @param {number} type - The data type of value, see Dish enums. - */ -const Dish = function(value, type) { - this.value = value || typeof value === "string" ? value : null; - this.type = type || Dish.BYTE_ARRAY; -}; - - -/** - * Dish data type enum for byte arrays. - * @readonly - * @enum - */ -Dish.BYTE_ARRAY = 0; -/** - * Dish data type enum for strings. - * @readonly - * @enum - */ -Dish.STRING = 1; -/** - * Dish data type enum for numbers. - * @readonly - * @enum - */ -Dish.NUMBER = 2; -/** - * Dish data type enum for HTML. - * @readonly - * @enum - */ -Dish.HTML = 3; -/** - * Dish data type enum for ArrayBuffers. - * @readonly - * @enum - */ -Dish.ARRAY_BUFFER = 4; -/** - * Dish data type enum for BigNumbers. - * @readonly - * @enum - */ -Dish.BIG_NUMBER = 5; - - -/** - * Returns the data type enum for the given type string. - * - * @static - * @param {string} typeStr - The name of the data type. - * @returns {number} The data type enum value. - */ -Dish.typeEnum = function(typeStr) { - switch (typeStr.toLowerCase()) { - case "bytearray": - case "byte array": - return Dish.BYTE_ARRAY; - case "string": - return Dish.STRING; - case "number": - return Dish.NUMBER; - case "html": - return Dish.HTML; - case "arraybuffer": - case "array buffer": - return Dish.ARRAY_BUFFER; - case "bignumber": - case "big number": - return Dish.BIG_NUMBER; - default: - throw "Invalid data type string. No matching enum."; - } -}; - - -/** - * Returns the data type string for the given type enum. - * - * @static - * @param {number} typeEnum - The enum value of the data type. - * @returns {string} The data type as a string. - */ -Dish.enumLookup = function(typeEnum) { - switch (typeEnum) { - case Dish.BYTE_ARRAY: - return "byteArray"; - case Dish.STRING: - return "string"; - case Dish.NUMBER: - return "number"; - case Dish.HTML: - return "html"; - case Dish.ARRAY_BUFFER: - return "ArrayBuffer"; - case Dish.BIG_NUMBER: - return "BigNumber"; - default: - throw "Invalid data type enum. No matching type."; - } -}; - - -/** - * Sets the data value and type and then validates them. - * - * @param {byteArray|string|number|ArrayBuffer|BigNumber} value - The value of the input data. - * @param {number} type - The data type of value, see Dish enums. - */ -Dish.prototype.set = function(value, type) { - log.debug("Dish type: " + Dish.enumLookup(type)); - this.value = value; - this.type = type; - - if (!this.valid()) { - const sample = Utils.truncate(JSON.stringify(this.value), 13); - throw "Data is not a valid " + Dish.enumLookup(type) + ": " + sample; - } -}; - - -/** - * Returns the value of the data in the type format specified. - * - * @param {number} type - The data type of value, see Dish enums. - * @param {boolean} [notUTF8] - Do not treat strings as UTF8. - * @returns {byteArray|string|number|ArrayBuffer|BigNumber} The value of the output data. - */ -Dish.prototype.get = function(type, notUTF8) { - if (this.type !== type) { - this.translate(type, notUTF8); - } - return this.value; -}; - - -/** - * Translates the data to the given type format. - * - * @param {number} toType - The data type of value, see Dish enums. - * @param {boolean} [notUTF8] - Do not treat strings as UTF8. - */ -Dish.prototype.translate = function(toType, notUTF8) { - log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); - const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; - - // Convert data to intermediate byteArray type - switch (this.type) { - case Dish.STRING: - this.value = this.value ? Utils.strToByteArray(this.value) : []; - break; - case Dish.NUMBER: - this.value = typeof this.value == "number" ? Utils.strToByteArray(this.value.toString()) : []; - break; - case Dish.HTML: - this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - break; - case Dish.ARRAY_BUFFER: - // Array.from() would be nicer here, but it's slightly slower - this.value = Array.prototype.slice.call(new Uint8Array(this.value)); - break; - case Dish.BIG_NUMBER: - this.value = this.value instanceof BigNumber ? Utils.strToByteArray(this.value.toFixed()) : []; - break; - default: - break; - } - - this.type = Dish.BYTE_ARRAY; - - // Convert from byteArray to toType - switch (toType) { - case Dish.STRING: - case Dish.HTML: - this.value = this.value ? byteArrayToStr(this.value) : ""; - this.type = Dish.STRING; - break; - case Dish.NUMBER: - this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; - this.type = Dish.NUMBER; - break; - case Dish.ARRAY_BUFFER: - this.value = new Uint8Array(this.value).buffer; - this.type = Dish.ARRAY_BUFFER; - break; - case Dish.BIG_NUMBER: - try { - this.value = new BigNumber(byteArrayToStr(this.value)); - } catch (err) { - this.value = new BigNumber(NaN); - } - this.type = Dish.BIG_NUMBER; - break; - default: - break; - } -}; - - -/** - * Validates that the value is the type that has been specified. - * May have to disable parts of BYTE_ARRAY validation if it effects performance. - * - * @returns {boolean} Whether the data is valid or not. -*/ -Dish.prototype.valid = function() { - switch (this.type) { - case Dish.BYTE_ARRAY: - if (!(this.value instanceof Array)) { - return false; - } - - // Check that every value is a number between 0 - 255 - for (let i = 0; i < this.value.length; i++) { - if (typeof this.value[i] !== "number" || - this.value[i] < 0 || - this.value[i] > 255) { - return false; - } - } - return true; - case Dish.STRING: - case Dish.HTML: - return typeof this.value === "string"; - case Dish.NUMBER: - return typeof this.value === "number"; - case Dish.ARRAY_BUFFER: - return this.value instanceof ArrayBuffer; - case Dish.BIG_NUMBER: - return this.value instanceof BigNumber; - default: - return false; - } -}; - - -/** - * Determines how much space the Dish takes up. - * Numbers in JavaScript are 64-bit floating point, however for the purposes of the Dish, - * we measure how many bytes are taken up when the number is written as a string. - * - * @returns {number} -*/ -Dish.prototype.size = function() { - switch (this.type) { - case Dish.BYTE_ARRAY: - case Dish.STRING: - case Dish.HTML: - return this.value.length; - case Dish.NUMBER: - case Dish.BIG_NUMBER: - return this.value.toString().length; - case Dish.ARRAY_BUFFER: - return this.value.byteLength; - default: - return -1; - } -}; - - -export default Dish; diff --git a/src/core/Dish.mjs b/src/core/Dish.mjs new file mode 100755 index 00000000..792395c1 --- /dev/null +++ b/src/core/Dish.mjs @@ -0,0 +1,293 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @author Matt C [matt@artemisbot.uk] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Utils from "./Utils"; +import BigNumber from "bignumber.js"; +import log from "loglevel"; + +/** + * The data being operated on by each operation. + */ +class Dish { + + /** + * Dish constructor + * + * @param {byteArray|string|number|ArrayBuffer|BigNumber} [value=null] + * - The value of the input data. + * @param {number} [type=Dish.BYTE_ARRAY] + * - The data type of value, see Dish enums. + */ + constructor(value=null, type=Dish.BYTE_ARRAY) { + this.value = value; + this.type = type; + } + + + /** + * Returns the data type enum for the given type string. + * + * @param {string} typeStr - The name of the data type. + * @returns {number} The data type enum value. + */ + static typeEnum(typeStr) { + switch (typeStr.toLowerCase()) { + case "bytearray": + case "byte array": + return Dish.BYTE_ARRAY; + case "string": + return Dish.STRING; + case "number": + return Dish.NUMBER; + case "html": + return Dish.HTML; + case "arraybuffer": + case "array buffer": + return Dish.ARRAY_BUFFER; + case "bignumber": + case "big number": + return Dish.BIG_NUMBER; + default: + throw "Invalid data type string. No matching enum."; + } + } + + + /** + * Returns the data type string for the given type enum. + * + * @param {number} typeEnum - The enum value of the data type. + * @returns {string} The data type as a string. + */ + static enumLookup(typeEnum) { + switch (typeEnum) { + case Dish.BYTE_ARRAY: + return "byteArray"; + case Dish.STRING: + return "string"; + case Dish.NUMBER: + return "number"; + case Dish.HTML: + return "html"; + case Dish.ARRAY_BUFFER: + return "ArrayBuffer"; + case Dish.BIG_NUMBER: + return "BigNumber"; + default: + throw "Invalid data type enum. No matching type."; + } + } + + + /** + * Sets the data value and type and then validates them. + * + * @param {byteArray|string|number|ArrayBuffer|BigNumber} value + * - The value of the input data. + * @param {number} type + * - The data type of value, see Dish enums. + */ + set(value, type) { + if (typeof type === "string") { + type = Dish.typeEnum(type); + } + + log.debug("Dish type: " + Dish.enumLookup(type)); + this.value = value; + this.type = type; + + if (!this.valid()) { + const sample = Utils.truncate(JSON.stringify(this.value), 13); + throw "Data is not a valid " + Dish.enumLookup(type) + ": " + sample; + } + } + + + /** + * Returns the value of the data in the type format specified. + * + * @param {number} type - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + * @returns {byteArray|string|number|ArrayBuffer|BigNumber} + * The value of the output data. + */ + get(type, notUTF8=false) { + if (typeof type === "string") { + type = Dish.typeEnum(type); + } + if (this.type !== type) { + this.translate(type, notUTF8); + } + return this.value; + } + + + /** + * Translates the data to the given type format. + * + * @param {number} toType - The data type of value, see Dish enums. + * @param {boolean} [notUTF8=false] - Do not treat strings as UTF8. + */ + translate(toType, notUTF8=false) { + log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`); + const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8; + + // Convert data to intermediate byteArray type + switch (this.type) { + case Dish.STRING: + this.value = this.value ? Utils.strToByteArray(this.value) : []; + break; + case Dish.NUMBER: + this.value = typeof this.value == "number" ? Utils.strToByteArray(this.value.toString()) : []; + break; + case Dish.HTML: + this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); + break; + case Dish.BIG_NUMBER: + this.value = this.value instanceof BigNumber ? Utils.strToByteArray(this.value.toFixed()) : []; + break; + default: + break; + } + + this.type = Dish.BYTE_ARRAY; + + // Convert from byteArray to toType + switch (toType) { + case Dish.STRING: + case Dish.HTML: + this.value = this.value ? byteArrayToStr(this.value) : ""; + this.type = Dish.STRING; + break; + case Dish.NUMBER: + this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0; + this.type = Dish.NUMBER; + break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; + case Dish.BIG_NUMBER: + try { + this.value = new BigNumber(byteArrayToStr(this.value)); + } catch (err) { + this.value = new BigNumber(NaN); + } + this.type = Dish.BIG_NUMBER; + break; + default: + break; + } + } + + + /** + * Validates that the value is the type that has been specified. + * May have to disable parts of BYTE_ARRAY validation if it effects performance. + * + * @returns {boolean} Whether the data is valid or not. + */ + valid() { + switch (this.type) { + case Dish.BYTE_ARRAY: + if (!(this.value instanceof Array)) { + return false; + } + + // Check that every value is a number between 0 - 255 + for (let i = 0; i < this.value.length; i++) { + if (typeof this.value[i] !== "number" || + this.value[i] < 0 || + this.value[i] > 255) { + return false; + } + } + return true; + case Dish.STRING: + case Dish.HTML: + return typeof this.value === "string"; + case Dish.NUMBER: + return typeof this.value === "number"; + case Dish.ARRAY_BUFFER: + return this.value instanceof ArrayBuffer; + case Dish.BIG_NUMBER: + return this.value instanceof BigNumber; + default: + return false; + } + } + + + /** + * Determines how much space the Dish takes up. + * Numbers in JavaScript are 64-bit floating point, however for the purposes of the Dish, + * we measure how many bytes are taken up when the number is written as a string. + * + * @returns {number} + */ + get size() { + switch (this.type) { + case Dish.BYTE_ARRAY: + case Dish.STRING: + case Dish.HTML: + return this.value.length; + case Dish.NUMBER: + case Dish.BIG_NUMBER: + return this.value.toString().length; + case Dish.ARRAY_BUFFER: + return this.value.byteLength; + default: + return -1; + } + } + +} + + +/** + * Dish data type enum for byte arrays. + * @readonly + * @enum + */ +Dish.BYTE_ARRAY = 0; +/** + * Dish data type enum for strings. + * @readonly + * @enum + */ +Dish.STRING = 1; +/** + * Dish data type enum for numbers. + * @readonly + * @enum + */ +Dish.NUMBER = 2; +/** + * Dish data type enum for HTML. + * @readonly + * @enum + */ +Dish.HTML = 3; +/** + * Dish data type enum for ArrayBuffers. + * @readonly + * @enum + */ +Dish.ARRAY_BUFFER = 4; +/** + * Dish data type enum for BigNumbers. + * @readonly + * @enum + */ +Dish.BIG_NUMBER = 5; + + +export default Dish; diff --git a/src/core/FlowControl.js b/src/core/FlowControl.js index fd2c0785..daa1c343 100755 --- a/src/core/FlowControl.js +++ b/src/core/FlowControl.js @@ -1,5 +1,5 @@ -import Recipe from "./Recipe.js"; -import Dish from "./Dish.js"; +import Recipe from "./Recipe"; +import Dish from "./Dish"; /** @@ -27,7 +27,7 @@ const FlowControl = { inputType = opList[state.progress].inputType, outputType = opList[state.progress].outputType, input = state.dish.get(inputType), - ings = opList[state.progress].getIngValues(), + ings = opList[state.progress].ingValues, splitDelim = ings[0], mergeDelim = ings[1], ignoreErrors = ings[2], @@ -41,7 +41,7 @@ const FlowControl = { // Create subOpList for each tranche to operate on // (all remaining operations unless we encounter a Merge) for (i = state.progress + 1; i < opList.length; i++) { - if (opList[i].name === "Merge" && !opList[i].isDisabled()) { + if (opList[i].name === "Merge" && !opList[i].disabled) { break; } else { subOpList.push(opList[i]); @@ -57,7 +57,7 @@ const FlowControl = { recipe.addOperations(subOpList); // Take a deep(ish) copy of the ingredient values - const ingValues = subOpList.map(op => JSON.parse(JSON.stringify(op.getIngValues()))); + const ingValues = subOpList.map(op => JSON.parse(JSON.stringify(op.ingValues))); // Run recipe over each tranche for (i = 0; i < inputs.length; i++) { @@ -65,7 +65,7 @@ const FlowControl = { // Baseline ing values for each tranche so that registers are reset subOpList.forEach((op, i) => { - op.setIngValues(JSON.parse(JSON.stringify(ingValues[i]))); + op.ingValues = JSON.parse(JSON.stringify(ingValues[i])); }); const dish = new Dish(inputs[i], inputType); @@ -112,7 +112,7 @@ const FlowControl = { * @returns {Object} The updated state of the recipe. */ runRegister: function(state) { - const ings = state.opList[state.progress].getIngValues(), + const ings = state.opList[state.progress].ingValues, extractorStr = ings[0], i = ings[1], m = ings[2]; @@ -150,9 +150,9 @@ const FlowControl = { // Step through all subsequent ops and replace registers in args with extracted content for (let i = state.progress + 1; i < state.opList.length; i++) { - if (state.opList[i].isDisabled()) continue; + if (state.opList[i].disabled) continue; - let args = state.opList[i].getIngValues(); + let args = state.opList[i].ingValues; args = args.map(arg => { if (typeof arg !== "string" && typeof arg !== "object") return arg; @@ -181,7 +181,7 @@ const FlowControl = { * @returns {Object} The updated state of the recipe. */ runJump: function(state) { - const ings = state.opList[state.progress].getIngValues(), + const ings = state.opList[state.progress].ingValues, label = ings[0], maxJumps = ings[1], jmpIndex = FlowControl._getLabelIndex(label, state); @@ -209,7 +209,7 @@ const FlowControl = { * @returns {Object} The updated state of the recipe. */ runCondJump: function(state) { - const ings = state.opList[state.progress].getIngValues(), + const ings = state.opList[state.progress].ingValues, dish = state.dish, regexStr = ings[0], invert = ings[1], @@ -276,7 +276,7 @@ const FlowControl = { for (let o = 0; o < state.opList.length; o++) { let operation = state.opList[o]; if (operation.name === "Label"){ - let ings = operation.getIngValues(); + let ings = operation.ingValues; if (name === ings[0]) { return o; } diff --git a/src/core/Ingredient.js b/src/core/Ingredient.js deleted file mode 100755 index e8d8a8cc..00000000 --- a/src/core/Ingredient.js +++ /dev/null @@ -1,92 +0,0 @@ -import Utils from "./Utils.js"; - - -/** - * The arguments to operations. - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @class - * @param {Object} ingredientConfig - */ -const Ingredient = function(ingredientConfig) { - this.name = ""; - this.type = ""; - this.value = null; - - if (ingredientConfig) { - this._parseConfig(ingredientConfig); - } -}; - - -/** - * Reads and parses the given config. - * - * @private - * @param {Object} ingredientConfig - */ -Ingredient.prototype._parseConfig = function(ingredientConfig) { - this.name = ingredientConfig.name; - this.type = ingredientConfig.type; -}; - - -/** - * Returns the value of the Ingredient as it should be displayed in a recipe config. - * - * @returns {*} - */ -Ingredient.prototype.getConfig = function() { - return this.value; -}; - - -/** - * Sets the value of the Ingredient. - * - * @param {*} value - */ -Ingredient.prototype.setValue = function(value) { - this.value = Ingredient.prepare(value, this.type); -}; - - -/** - * Most values will be strings when they are entered. This function converts them to the correct - * type. - * - * @static - * @param {*} data - * @param {string} type - The name of the data type. -*/ -Ingredient.prepare = function(data, type) { - let number; - - switch (type) { - case "binaryString": - case "binaryShortString": - case "editableOption": - return Utils.parseEscapedChars(data); - case "byteArray": - if (typeof data == "string") { - data = data.replace(/\s+/g, ""); - return Utils.fromHex(data); - } else { - return data; - } - case "number": - number = parseFloat(data); - if (isNaN(number)) { - const sample = Utils.truncate(data.toString(), 10); - throw "Invalid ingredient value. Not a number: " + sample; - } - return number; - default: - return data; - } -}; - -export default Ingredient; diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs new file mode 100755 index 00000000..af96beaf --- /dev/null +++ b/src/core/Ingredient.mjs @@ -0,0 +1,109 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Utils from "./Utils"; + +/** + * The arguments to operations. + */ +class Ingredient { + + /** + * Ingredient constructor + * + * @param {Object} ingredientConfig + */ + constructor(ingredientConfig) { + this.name = ""; + this.type = ""; + this._value = null; + + if (ingredientConfig) { + this._parseConfig(ingredientConfig); + } + } + + + /** + * Reads and parses the given config. + * + * @private + * @param {Object} ingredientConfig + */ + _parseConfig(ingredientConfig) { + this.name = ingredientConfig.name; + this.type = ingredientConfig.type; + this.defaultValue = ingredientConfig.value; + } + + + /** + * Returns the value of the Ingredient as it should be displayed in a recipe config. + * + * @returns {*} + */ + get config() { + return this._value; + } + + + /** + * Sets the value of the Ingredient. + * + * @param {*} value + */ + set value(value) { + this._value = Ingredient.prepare(value, this.type); + } + + + /** + * Gets the value of the Ingredient. + * + * @returns {*} + */ + get value() { + return this._value; + } + + + /** + * Most values will be strings when they are entered. This function converts them to the correct + * type. + * + * @param {*} data + * @param {string} type - The name of the data type. + */ + static prepare(data, type) { + let number; + + switch (type) { + case "binaryString": + case "binaryShortString": + case "editableOption": + return Utils.parseEscapedChars(data); + case "byteArray": + if (typeof data == "string") { + data = data.replace(/\s+/g, ""); + return Utils.fromHex(data); + } else { + return data; + } + case "number": + number = parseFloat(data); + if (isNaN(number)) { + const sample = Utils.truncate(data.toString(), 10); + throw "Invalid ingredient value. Not a number: " + sample; + } + return number; + default: + return data; + } + } + +} + +export default Ingredient; diff --git a/src/core/Operation.js b/src/core/Operation.js deleted file mode 100755 index d4ee21d3..00000000 --- a/src/core/Operation.js +++ /dev/null @@ -1,174 +0,0 @@ -import Dish from "./Dish.js"; -import Ingredient from "./Ingredient.js"; -import OperationConfig from "./config/MetaConfig.js"; -import OpModules from "./config/modules/OpModules.js"; - - -/** - * The Operation specified by the user to be run. - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @class - * @param {string} operationName - */ -const Operation = function(operationName) { - this.name = operationName; - this.module = ""; - this.description = ""; - this.inputType = -1; - this.outputType = -1; - this.run = null; - this.highlight = null; - this.highlightReverse = null; - this.breakpoint = false; - this.disabled = false; - this.ingList = []; - - if (OperationConfig.hasOwnProperty(this.name)) { - this._parseConfig(OperationConfig[this.name]); - } -}; - - -/** - * Reads and parses the given config. - * - * @private - * @param {Object} operationConfig - */ -Operation.prototype._parseConfig = function(operationConfig) { - this.module = operationConfig.module; - this.description = operationConfig.description; - this.inputType = Dish.typeEnum(operationConfig.inputType); - this.outputType = Dish.typeEnum(operationConfig.outputType); - this.highlight = operationConfig.highlight; - this.highlightReverse = operationConfig.highlightReverse; - this.flowControl = operationConfig.flowControl; - this.run = OpModules[this.module][this.name]; - - for (let a = 0; a < operationConfig.args.length; a++) { - const ingredientConfig = operationConfig.args[a]; - const ingredient = new Ingredient(ingredientConfig); - this.addIngredient(ingredient); - } - - if (this.highlight === "func") { - this.highlight = OpModules[this.module][`${this.name}-highlight`]; - } - - if (this.highlightReverse === "func") { - this.highlightReverse = OpModules[this.module][`${this.name}-highlightReverse`]; - } -}; - - -/** - * Returns the value of the Operation as it should be displayed in a recipe config. - * - * @returns {Object} - */ -Operation.prototype.getConfig = function() { - const ingredientConfig = []; - - for (let o = 0; o < this.ingList.length; o++) { - ingredientConfig.push(this.ingList[o].getConfig()); - } - - const operationConfig = { - "op": this.name, - "args": ingredientConfig - }; - - return operationConfig; -}; - - -/** - * Adds a new Ingredient to this Operation. - * - * @param {Ingredient} ingredient - */ -Operation.prototype.addIngredient = function(ingredient) { - this.ingList.push(ingredient); -}; - - -/** - * Set the Ingredient values for this Operation. - * - * @param {Object[]} ingValues - */ -Operation.prototype.setIngValues = function(ingValues) { - for (let i = 0; i < ingValues.length; i++) { - this.ingList[i].setValue(ingValues[i]); - } -}; - - -/** - * Get the Ingredient values for this Operation. - * - * @returns {Object[]} - */ -Operation.prototype.getIngValues = function() { - const ingValues = []; - for (let i = 0; i < this.ingList.length; i++) { - ingValues.push(this.ingList[i].value); - } - return ingValues; -}; - - -/** - * Set whether this Operation has a breakpoint. - * - * @param {boolean} value - */ -Operation.prototype.setBreakpoint = function(value) { - this.breakpoint = !!value; -}; - - -/** - * Returns true if this Operation has a breakpoint set. - * - * @returns {boolean} - */ -Operation.prototype.isBreakpoint = function() { - return this.breakpoint; -}; - - -/** - * Set whether this Operation is disabled. - * - * @param {boolean} value - */ -Operation.prototype.setDisabled = function(value) { - this.disabled = !!value; -}; - - -/** - * Returns true if this Operation is disabled. - * - * @returns {boolean} - */ -Operation.prototype.isDisabled = function() { - return this.disabled; -}; - - -/** - * Returns true if this Operation is a flow control. - * - * @returns {boolean} - */ -Operation.prototype.isFlowControl = function() { - return this.flowControl; -}; - -export default Operation; diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs new file mode 100755 index 00000000..30ad71e4 --- /dev/null +++ b/src/core/Operation.mjs @@ -0,0 +1,239 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +import Dish from "./Dish"; +import Ingredient from "./Ingredient"; + +/** + * The Operation specified by the user to be run. + */ +class Operation { + + /** + * Operation constructor + */ + constructor() { + // Private fields + this._inputType = -1; + this._outputType = -1; + this._breakpoint = false; + this._disabled = false; + this._flowControl = false; + this._ingList = []; + + // Public fields + this.name = ""; + this.module = ""; + this.description = ""; + } + + + /** + * Interface for operation runner + * + * @param {*} input + * @param {Object[]} args + * @returns {*} + */ + run(input, args) { + return input; + } + + + /** + * Interface for forward highlighter + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlight(pos, args) { + return false; + } + + + /** + * Interface for reverse highlighter + * + * @param {Object[]} pos + * @param {number} pos[].start + * @param {number} pos[].end + * @param {Object[]} args + * @returns {Object[]} pos + */ + highlightReverse(pos, args) { + return false; + } + + + /** + * Sets the input type as a Dish enum. + * + * @param {string} typeStr + */ + set inputType(typeStr) { + this._inputType = Dish.typeEnum(typeStr); + } + + + /** + * Gets the input type as a readable string. + * + * @returns {string} + */ + get inputType() { + return Dish.enumLookup(this._inputType); + } + + + /** + * Sets the output type as a Dish enum. + * + * @param {string} typeStr + */ + set outputType(typeStr) { + this._outputType = Dish.typeEnum(typeStr); + } + + + /** + * Gets the output type as a readable string. + * + * @returns {string} + */ + get outputType() { + return Dish.enumLookup(this._outputType); + } + + + /** + * Sets the args for the current operation. + * + * @param {Object[]} conf + */ + set args(conf) { + conf.forEach(arg => { + const ingredient = new Ingredient(arg); + this.addIngredient(ingredient); + }); + } + + + /** + * Gets the args for the current operation. + * + * @param {Object[]} conf + */ + get args() { + return this._ingList.map(ing => { + return { + name: ing.name, + type: ing.type, + value: ing.defaultValue + }; + }); + } + + + /** + * Returns the value of the Operation as it should be displayed in a recipe config. + * + * @returns {Object} + */ + get config() { + return { + "op": this.name, + "args": this._ingList.map(ing => ing.conf) + }; + } + + + /** + * Adds a new Ingredient to this Operation. + * + * @param {Ingredient} ingredient + */ + addIngredient(ingredient) { + this._ingList.push(ingredient); + } + + + /** + * Set the Ingredient values for this Operation. + * + * @param {Object[]} ingValues + */ + set ingValues(ingValues) { + ingValues.forEach((val, i) => { + this._ingList[i].value = val; + }); + } + + + /** + * Get the Ingredient values for this Operation. + * + * @returns {Object[]} + */ + get ingValues() { + return this._ingList.map(ing => ing.value); + } + + + /** + * Set whether this Operation has a breakpoint. + * + * @param {boolean} value + */ + set breakpoint(value) { + this._breakpoint = !!value; + } + + + /** + * Returns true if this Operation has a breakpoint set. + * + * @returns {boolean} + */ + get breakpoint() { + return this._breakpoint; + } + + + /** + * Set whether this Operation is disabled. + * + * @param {boolean} value + */ + set disabled(value) { + this._disabled = !!value; + } + + + /** + * Returns true if this Operation is disabled. + * + * @returns {boolean} + */ + get disabled() { + return this._disabled; + } + + + /** + * Returns true if this Operation is a flow control. + * + * @returns {boolean} + */ + get flowControl() { + return this._flowControl; + } + +} + +export default Operation; diff --git a/src/core/Recipe.js b/src/core/Recipe.js deleted file mode 100755 index 9305c32d..00000000 --- a/src/core/Recipe.js +++ /dev/null @@ -1,264 +0,0 @@ -import Operation from "./Operation.js"; - - -/** - * The Recipe controls a list of Operations and the Dish they operate on. - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @class - * @param {Object} recipeConfig - */ -const Recipe = function(recipeConfig) { - this.opList = []; - - if (recipeConfig) { - this._parseConfig(recipeConfig); - } -}; - - -/** - * Reads and parses the given config. - * - * @private - * @param {Object} recipeConfig - */ -Recipe.prototype._parseConfig = function(recipeConfig) { - for (let c = 0; c < recipeConfig.length; c++) { - const operationName = recipeConfig[c].op; - const operation = new Operation(operationName); - operation.setIngValues(recipeConfig[c].args); - operation.setBreakpoint(recipeConfig[c].breakpoint); - operation.setDisabled(recipeConfig[c].disabled); - this.addOperation(operation); - } -}; - - -/** - * Returns the value of the Recipe as it should be displayed in a recipe config. - * - * @returns {*} - */ -Recipe.prototype.getConfig = function() { - const recipeConfig = []; - - for (let o = 0; o < this.opList.length; o++) { - recipeConfig.push(this.opList[o].getConfig()); - } - - return recipeConfig; -}; - - -/** - * Adds a new Operation to this Recipe. - * - * @param {Operation} operation - */ -Recipe.prototype.addOperation = function(operation) { - this.opList.push(operation); -}; - - -/** - * Adds a list of Operations to this Recipe. - * - * @param {Operation[]} operations - */ -Recipe.prototype.addOperations = function(operations) { - this.opList = this.opList.concat(operations); -}; - - -/** - * Set a breakpoint on a specified Operation. - * - * @param {number} position - The index of the Operation - * @param {boolean} value - */ -Recipe.prototype.setBreakpoint = function(position, value) { - try { - this.opList[position].setBreakpoint(value); - } catch (err) { - // Ignore index error - } -}; - - -/** - * Remove breakpoints on all Operations in the Recipe up to the specified position. Used by Flow - * Control Fork operation. - * - * @param {number} pos - */ -Recipe.prototype.removeBreaksUpTo = function(pos) { - for (let i = 0; i < pos; i++) { - this.opList[i].setBreakpoint(false); - } -}; - - -/** - * Returns true if there is an Flow Control Operation in this Recipe. - * - * @returns {boolean} - */ -Recipe.prototype.containsFlowControl = function() { - for (let i = 0; i < this.opList.length; i++) { - if (this.opList[i].isFlowControl()) return true; - } - return false; -}; - - -/** - * Returns the index of the last Operation index that will be executed, taking into account disabled - * Operations and breakpoints. - * - * @param {number} [startIndex=0] - The index to start searching from - * @returns (number} - */ -Recipe.prototype.lastOpIndex = function(startIndex) { - let i = startIndex + 1 || 0, - op; - - for (; i < this.opList.length; i++) { - op = this.opList[i]; - if (op.isDisabled()) return i-1; - if (op.isBreakpoint()) return i-1; - } - - return i-1; -}; - - -/** - * Executes each operation in the recipe over the given Dish. - * - * @param {Dish} dish - * @param {number} [startFrom=0] - The index of the Operation to start executing from - * @param {number} [forkState={}] - If this is a forked recipe, the state of the recipe up to this point - * @returns {number} - The final progress through the recipe - */ -Recipe.prototype.execute = async function(dish, startFrom = 0, forkState = {}) { - let op, input, output, - numJumps = 0, - numRegisters = forkState.numRegisters || 0; - - log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`); - - for (let i = startFrom; i < this.opList.length; i++) { - op = this.opList[i]; - log.debug(`[${i}] ${op.name} ${JSON.stringify(op.getIngValues())}`); - if (op.isDisabled()) { - log.debug("Operation is disabled, skipping"); - continue; - } - if (op.isBreakpoint()) { - log.debug("Pausing at breakpoint"); - return i; - } - - try { - input = dish.get(op.inputType); - log.debug("Executing operation"); - - if (op.isFlowControl()) { - // Package up the current state - let state = { - "progress": i, - "dish": dish, - "opList": this.opList, - "numJumps": numJumps, - "numRegisters": numRegisters, - "forkOffset": forkState.forkOffset || 0 - }; - - state = await op.run(state); - i = state.progress; - numJumps = state.numJumps; - numRegisters = state.numRegisters; - } else { - output = await op.run(input, op.getIngValues()); - dish.set(output, op.outputType); - } - } catch (err) { - const e = typeof err == "string" ? { message: err } : err; - - e.progress = i; - if (e.fileName) { - e.displayStr = op.name + " - " + e.name + " in " + - e.fileName + " on line " + e.lineNumber + - ".

Message: " + (e.displayStr || e.message); - } else { - e.displayStr = op.name + " - " + (e.displayStr || e.message); - } - - throw e; - } - } - - log.debug("Recipe complete"); - return this.opList.length; -}; - - -/** - * Returns the recipe configuration in string format. - * - * @returns {string} - */ -Recipe.prototype.toString = function() { - return JSON.stringify(this.getConfig()); -}; - - -/** - * Creates a Recipe from a given configuration string. - * - * @param {string} recipeStr - */ -Recipe.prototype.fromString = function(recipeStr) { - const recipeConfig = JSON.parse(recipeStr); - this._parseConfig(recipeConfig); -}; - - -/** - * Generates a list of all the highlight functions assigned to operations in the recipe, if the - * entire recipe supports highlighting. - * - * @returns {Object[]} highlights - * @returns {function} highlights[].f - * @returns {function} highlights[].b - * @returns {Object[]} highlights[].args - */ -Recipe.prototype.generateHighlightList = function() { - const highlights = []; - - for (let i = 0; i < this.opList.length; i++) { - let op = this.opList[i]; - if (op.isDisabled()) continue; - - // If any breakpoints are set, do not attempt to highlight - if (op.isBreakpoint()) return false; - - // If any of the operations do not support highlighting, fail immediately. - if (op.highlight === false || op.highlight === undefined) return false; - - highlights.push({ - f: op.highlight, - b: op.highlightReverse, - args: op.getIngValues() - }); - } - - return highlights; -}; - - -export default Recipe; diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs new file mode 100755 index 00000000..ce03f8bf --- /dev/null +++ b/src/core/Recipe.mjs @@ -0,0 +1,250 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + +// import Operation from "./Operation.js"; +import OpModules from "./config/modules/OpModules"; +import OperationConfig from "./config/OperationConfig.json"; +import log from "loglevel"; + +/** + * The Recipe controls a list of Operations and the Dish they operate on. + */ +class Recipe { + + /** + * Recipe constructor + * + * @param {Object} recipeConfig + */ + constructor(recipeConfig) { + this.opList = []; + + if (recipeConfig) { + this._parseConfig(recipeConfig); + } + } + + + /** + * Reads and parses the given config. + * + * @private + * @param {Object} recipeConfig + */ + _parseConfig(recipeConfig) { + for (let c = 0; c < recipeConfig.length; c++) { + const operationName = recipeConfig[c].op; + const opConf = OperationConfig[operationName]; + const opObj = OpModules[opConf.module][operationName]; + const operation = new opObj(); + operation.ingValues = recipeConfig[c].args; + operation.breakpoint = recipeConfig[c].breakpoint; + operation.disabled = recipeConfig[c].disabled; + this.addOperation(operation); + } + } + + + /** + * Returns the value of the Recipe as it should be displayed in a recipe config. + * + * @returns {Object[]} + */ + get config() { + return this.opList.map(op => op.config); + } + + + /** + * Adds a new Operation to this Recipe. + * + * @param {Operation} operation + */ + addOperation(operation) { + this.opList.push(operation); + } + + + /** + * Adds a list of Operations to this Recipe. + * + * @param {Operation[]} operations + */ + addOperations(operations) { + this.opList = this.opList.concat(operations); + } + + + /** + * Set a breakpoint on a specified Operation. + * + * @param {number} position - The index of the Operation + * @param {boolean} value + */ + setBreakpoint(position, value) { + try { + this.opList[position].breakpoint = value; + } catch (err) { + // Ignore index error + } + } + + + /** + * Remove breakpoints on all Operations in the Recipe up to the specified position. Used by Flow + * Control Fork operation. + * + * @param {number} pos + */ + removeBreaksUpTo(pos) { + for (let i = 0; i < pos; i++) { + this.opList[i].breakpoint = false; + } + } + + + /** + * Returns true if there is an Flow Control Operation in this Recipe. + * + * @returns {boolean} + */ + containsFlowControl() { + return this.opList.reduce((acc, curr) => { + return acc || curr.flowControl; + }, false); + } + + + /** + * Executes each operation in the recipe over the given Dish. + * + * @param {Dish} dish + * @param {number} [startFrom=0] + * - The index of the Operation to start executing from + * @param {number} [forkState={}] + * - If this is a forked recipe, the state of the recipe up to this point + * @returns {number} + * - The final progress through the recipe + */ + async execute(dish, startFrom=0, forkState={}) { + let op, input, output, + numJumps = 0, + numRegisters = forkState.numRegisters || 0; + + log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`); + + for (let i = startFrom; i < this.opList.length; i++) { + op = this.opList[i]; + log.debug(`[${i}] ${op.name} ${JSON.stringify(op.ingValues)}`); + if (op.disabled) { + log.debug("Operation is disabled, skipping"); + continue; + } + if (op.breakpoint) { + log.debug("Pausing at breakpoint"); + return i; + } + + try { + input = dish.get(op.inputType); + log.debug("Executing operation"); + + if (op.flowControl) { + // Package up the current state + let state = { + "progress": i, + "dish": dish, + "opList": this.opList, + "numJumps": numJumps, + "numRegisters": numRegisters, + "forkOffset": forkState.forkOffset || 0 + }; + + state = await op.run(state); + i = state.progress; + numJumps = state.numJumps; + numRegisters = state.numRegisters; + } else { + output = await op.run(input, op.ingValues); + dish.set(output, op.outputType); + } + } catch (err) { + const e = typeof err == "string" ? { message: err } : err; + + e.progress = i; + if (e.fileName) { + e.displayStr = op.name + " - " + e.name + " in " + + e.fileName + " on line " + e.lineNumber + + ".

Message: " + (e.displayStr || e.message); + } else { + e.displayStr = op.name + " - " + (e.displayStr || e.message); + } + + throw e; + } + } + + log.debug("Recipe complete"); + return this.opList.length; + } + + + /** + * Returns the recipe configuration in string format. + * + * @returns {string} + */ + toString() { + return JSON.stringify(this.config); + } + + + /** + * Creates a Recipe from a given configuration string. + * + * @param {string} recipeStr + */ + fromString(recipeStr) { + const recipeConfig = JSON.parse(recipeStr); + this._parseConfig(recipeConfig); + } + + + /** + * Generates a list of all the highlight functions assigned to operations in the recipe, if the + * entire recipe supports highlighting. + * + * @returns {Object[]} highlights + * @returns {function} highlights[].f + * @returns {function} highlights[].b + * @returns {Object[]} highlights[].args + */ + generateHighlightList() { + const highlights = []; + + for (let i = 0; i < this.opList.length; i++) { + let op = this.opList[i]; + if (op.disabled) continue; + + // If any breakpoints are set, do not attempt to highlight + if (op.breakpoint) return false; + + // If any of the operations do not support highlighting, fail immediately. + if (op.highlight === false || op.highlight === undefined) return false; + + highlights.push({ + f: op.highlight, + b: op.highlightReverse, + args: op.ingValues + }); + } + + return highlights; + } + +} + +export default Recipe; diff --git a/src/core/Utils.js b/src/core/Utils.mjs similarity index 91% rename from src/core/Utils.js rename to src/core/Utils.mjs index 6d80c21b..116d856c 100755 --- a/src/core/Utils.js +++ b/src/core/Utils.mjs @@ -1,17 +1,17 @@ +/** + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2016 + * @license Apache-2.0 + */ + import utf8 from "utf8"; import moment from "moment-timezone"; /** * Utility functions for use in operations, the core framework and the stage. - * - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - * - * @namespace */ -const Utils = { +class Utils { /** * Translates an ordinal into a character. @@ -23,7 +23,7 @@ const Utils = { * // returns 'a' * Utils.chr(97); */ - chr: function(o) { + static chr(o) { // Detect astral symbols // Thanks to @mathiasbynens for this solution // https://mathiasbynens.be/notes/javascript-unicode @@ -35,7 +35,7 @@ const Utils = { } return String.fromCharCode(o); - }, + } /** @@ -48,7 +48,7 @@ const Utils = { * // returns 97 * Utils.ord('a'); */ - ord: function(c) { + static ord(c) { // Detect astral symbols // Thanks to @mathiasbynens for this solution // https://mathiasbynens.be/notes/javascript-unicode @@ -62,7 +62,7 @@ const Utils = { } return c.charCodeAt(0); - }, + } /** @@ -88,8 +88,7 @@ const Utils = { * // returns ["t", "e", "s", "t", 1, 1, 1, 1] * Utils.padBytesRight("test", 8, 1); */ - padBytesRight: function(arr, numBytes, padByte) { - padByte = padByte || 0; + static padBytesRight(arr, numBytes, padByte=0) { const paddedBytes = new Array(numBytes); paddedBytes.fill(padByte); @@ -98,7 +97,7 @@ const Utils = { }); return paddedBytes; - }, + } /** @@ -116,13 +115,12 @@ const Utils = { * // returns "A long s-" * Utils.truncate("A long string", 9, "-"); */ - truncate: function(str, max, suffix) { - suffix = suffix || "..."; + static truncate(str, max, suffix="...") { if (str.length > max) { str = str.slice(0, max - suffix.length) + suffix; } return str; - }, + } /** @@ -139,11 +137,10 @@ const Utils = { * // returns "6e" * Utils.hex(110); */ - hex: function(c, length) { + static hex(c, length=2) { c = typeof c == "string" ? Utils.ord(c) : c; - length = length || 2; return c.toString(16).padStart(length, "0"); - }, + } /** @@ -160,11 +157,10 @@ const Utils = { * // returns "01101110" * Utils.bin(110); */ - bin: function(c, length) { + static bin(c, length=8) { c = typeof c == "string" ? Utils.ord(c) : c; - length = length || 8; return c.toString(2).padStart(length, "0"); - }, + } /** @@ -174,7 +170,7 @@ const Utils = { * @param {boolean} [preserveWs=false] - Whether or not to print whitespace. * @returns {string} */ - printable: function(str, preserveWs) { + static printable(str, preserveWs=false) { if (ENVIRONMENT_IS_WEB() && window.app && !window.app.options.treatAsUtf8) { str = Utils.byteArrayToChars(Utils.strToByteArray(str)); } @@ -185,7 +181,7 @@ const Utils = { str = str.replace(re, "."); if (!preserveWs) str = str.replace(wsRe, "."); return str; - }, + } /** @@ -201,7 +197,7 @@ const Utils = { * // returns "\n" * Utils.parseEscapedChars("\\n"); */ - parseEscapedChars: function(str) { + static parseEscapedChars(str) { return str.replace(/(\\)?\\([bfnrtv0'"]|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\})/g, function(m, a, b) { if (a === "\\") return "\\"+b; switch (b[0]) { @@ -232,7 +228,7 @@ const Utils = { return String.fromCharCode(parseInt(b.substr(1), 16)); } }); - }, + } /** @@ -246,9 +242,9 @@ const Utils = { * // returns "\[example\]" * Utils.escapeRegex("[example]"); */ - escapeRegex: function(str) { + static escapeRegex(str) { return str.replace(/([.*+?^=!:${}()|[\]/\\])/g, "\\$1"); - }, + } /** @@ -267,7 +263,7 @@ const Utils = { * // returns ["a", "b", "c", "d", "0", "-", "3"] * Utils.expandAlphRange("a-d0\\-3") */ - expandAlphRange: function(alphStr) { + static expandAlphRange(alphStr) { const alphArr = []; for (let i = 0; i < alphStr.length; i++) { @@ -291,7 +287,7 @@ const Utils = { } } return alphArr; - }, + } /** @@ -312,7 +308,7 @@ const Utils = { * // returns [208, 159, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130] * Utils.convertToByteArray("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64"); */ - convertToByteArray: function(str, type) { + static convertToByteArray(str, type) { switch (type.toLowerCase()) { case "hex": return Utils.fromHex(str); @@ -324,7 +320,7 @@ const Utils = { default: return Utils.strToByteArray(str); } - }, + } /** @@ -345,7 +341,7 @@ const Utils = { * // returns "Здравствуйте" * Utils.convertToByteString("0JfQtNGA0LDQstGB0YLQstGD0LnRgtC1", "base64"); */ - convertToByteString: function(str, type) { + static convertToByteString(str, type) { switch (type.toLowerCase()) { case "hex": return Utils.byteArrayToChars(Utils.fromHex(str)); @@ -357,7 +353,7 @@ const Utils = { default: return str; } - }, + } /** @@ -374,7 +370,7 @@ const Utils = { * // returns [228,189,160,229,165,189] * Utils.strToByteArray("你好"); */ - strToByteArray: function(str) { + static strToByteArray(str) { const byteArray = new Array(str.length); let i = str.length, b; while (i--) { @@ -384,7 +380,7 @@ const Utils = { if (b > 255) return Utils.strToUtf8ByteArray(str); } return byteArray; - }, + } /** @@ -400,7 +396,7 @@ const Utils = { * // returns [228,189,160,229,165,189] * Utils.strToUtf8ByteArray("你好"); */ - strToUtf8ByteArray: function(str) { + static strToUtf8ByteArray(str) { const utf8Str = utf8.encode(str); if (str.length !== utf8Str.length) { @@ -412,7 +408,7 @@ const Utils = { } return Utils.strToByteArray(utf8Str); - }, + } /** @@ -428,7 +424,7 @@ const Utils = { * // returns [20320,22909] * Utils.strToCharcode("你好"); */ - strToCharcode: function(str) { + static strToCharcode(str) { const charcode = []; for (let i = 0; i < str.length; i++) { @@ -446,7 +442,7 @@ const Utils = { } return charcode; - }, + } /** @@ -462,7 +458,7 @@ const Utils = { * // returns "你好" * Utils.byteArrayToUtf8([228,189,160,229,165,189]); */ - byteArrayToUtf8: function(byteArray) { + static byteArrayToUtf8(byteArray) { const str = Utils.byteArrayToChars(byteArray); try { const utf8Str = utf8.decode(str); @@ -479,7 +475,7 @@ const Utils = { // If it fails, treat it as ANSI return str; } - }, + } /** @@ -495,14 +491,14 @@ const Utils = { * // returns "你好" * Utils.byteArrayToChars([20320,22909]); */ - byteArrayToChars: function(byteArray) { + static byteArrayToChars(byteArray) { if (!byteArray) return ""; let str = ""; for (let i = 0; i < byteArray.length;) { str += String.fromCharCode(byteArray[i++]); } return str; - }, + } /** @@ -516,17 +512,17 @@ const Utils = { * // returns "hello" * Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer); */ - arrayBufferToStr: function(arrayBuffer, utf8=true) { + static arrayBufferToStr(arrayBuffer, utf8=true) { const byteArray = Array.prototype.slice.call(new Uint8Array(arrayBuffer)); return utf8 ? Utils.byteArrayToUtf8(byteArray) : Utils.byteArrayToChars(byteArray); - }, + } /** * Base64's the input byte array using the given alphabet, returning a string. * * @param {byteArray|Uint8Array|string} data - * @param {string} [alphabet] + * @param {string} [alphabet="A-Za-z0-9+/="] * @returns {string} * * @example @@ -536,15 +532,14 @@ const Utils = { * // returns "SGVsbG8=" * Utils.toBase64("Hello"); */ - toBase64: function(data, alphabet) { + static toBase64(data, alphabet="A-Za-z0-9+/=") { if (!data) return ""; if (typeof data == "string") { data = Utils.strToByteArray(data); } - alphabet = alphabet ? - Utils.expandAlphRange(alphabet).join("") : - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + alphabet = Utils.expandAlphRange(alphabet).join(""); + let output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, @@ -571,14 +566,14 @@ const Utils = { } return output; - }, + } /** * UnBase64's the input string using the given alphabet, returning a byte array. * * @param {byteArray} data - * @param {string} [alphabet] + * @param {string} [alphabet="A-Za-z0-9+/="] * @param {string} [returnType="string"] - Either "string" or "byteArray" * @param {boolean} [removeNonAlphChars=true] * @returns {byteArray} @@ -590,18 +585,12 @@ const Utils = { * // returns [72, 101, 108, 108, 111] * Utils.fromBase64("SGVsbG8=", null, "byteArray"); */ - fromBase64: function(data, alphabet, returnType, removeNonAlphChars) { - returnType = returnType || "string"; - + static fromBase64(data, alphabet="A-Za-z0-9+/=", returnType="string", removeNonAlphChars=true) { if (!data) { return returnType === "string" ? "" : []; } - alphabet = alphabet ? - Utils.expandAlphRange(alphabet).join("") : - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - if (removeNonAlphChars === undefined) - removeNonAlphChars = true; + alphabet = Utils.expandAlphRange(alphabet).join(""); let output = [], chr1, chr2, chr3, @@ -638,7 +627,7 @@ const Utils = { } return returnType === "string" ? Utils.byteArrayToUtf8(output) : output; - }, + } /** @@ -656,11 +645,9 @@ const Utils = { * // returns "0a:14:1e" * Utils.toHex([10,20,30], ":"); */ - toHex: function(data, delim, padding) { + static toHex(data, delim=" ", padding=2) { if (!data) return ""; - delim = typeof delim == "string" ? delim : " "; - padding = padding || 2; let output = ""; for (let i = 0; i < data.length; i++) { @@ -675,7 +662,7 @@ const Utils = { return output.slice(0, -delim.length); else return output; - }, + } /** @@ -688,7 +675,7 @@ const Utils = { * // returns "0a141e" * Utils.toHex([10,20,30]); */ - toHexFast: function(data) { + static toHexFast(data) { if (!data) return ""; const output = []; @@ -699,7 +686,7 @@ const Utils = { } return output.join(""); - }, + } /** @@ -717,11 +704,10 @@ const Utils = { * // returns [10,20,30] * Utils.fromHex("0a:14:1e", "Colon"); */ - fromHex: function(data, delim, byteLen) { + static fromHex(data, delim, byteLen=2) { delim = delim || (data.indexOf(" ") >= 0 ? "Space" : "None"); - byteLen = byteLen || 2; if (delim !== "None") { - const delimRegex = Utils.regexRep[delim]; + const delimRegex = Utils.regexRep(delim); data = data.replace(delimRegex, ""); } @@ -730,7 +716,7 @@ const Utils = { output.push(parseInt(data.substr(i, byteLen), 16)); } return output; - }, + } /** @@ -743,8 +729,7 @@ const Utils = { * // returns [["head1", "head2"], ["data1", "data2"]] * Utils.parseCSV("head1,head2\ndata1,data2"); */ - parseCSV: function(data) { - + static parseCSV(data) { let b, ignoreNext = false, inString = false, @@ -783,26 +768,27 @@ const Utils = { } return lines; - }, + } /** * Removes all HTML (or XML) tags from the input string. * * @param {string} htmlStr - * @param {boolean} removeScriptAndStyle - Flag to specify whether to remove entire script or style blocks + * @param {boolean} [removeScriptAndStyle=false] + * - Flag to specify whether to remove entire script or style blocks * @returns {string} * * @example * // returns "Test" * Utils.stripHtmlTags("
Test
"); */ - stripHtmlTags: function(htmlStr, removeScriptAndStyle) { + static stripHtmlTags(htmlStr, removeScriptAndStyle=false) { if (removeScriptAndStyle) { htmlStr = htmlStr.replace(/<(script|style)[^>]*>.*<\/(script|style)>/gmi, ""); } return htmlStr.replace(/<[^>]+>/g, ""); - }, + } /** @@ -816,7 +802,7 @@ const Utils = { * // return "A <script> tag" * Utils.escapeHtml("A