From 361a3b2929fc05946141880fb9006cf2ac96634b Mon Sep 17 00:00:00 2001 From: Kendall Goto Date: Wed, 9 Apr 2025 15:16:11 -0700 Subject: [PATCH] Add Template operation for basic JSON rendering --- package-lock.json | 30 ++++++++++++++-- package.json | 1 + src/core/config/Categories.json | 3 +- src/core/operations/Template.mjs | 53 +++++++++++++++++++++++++++++ tests/operations/index.mjs | 1 + tests/operations/tests/Template.mjs | 53 +++++++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 src/core/operations/Template.mjs create mode 100644 tests/operations/tests/Template.mjs diff --git a/package-lock.json b/package-lock.json index ef2da3f0..897fd4e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "handlebars": "^4.7.8", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", @@ -10842,6 +10843,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -13654,7 +13676,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, "license": "MIT" }, "node_modules/netmask": { @@ -16859,7 +16880,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -18877,6 +18897,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/worker-loader": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", diff --git a/package.json b/package.json index b3492a8e..3555a121 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "file-saver": "^2.0.5", "flat": "^6.0.1", "geodesy": "1.1.3", + "handlebars": "^4.7.8", "highlight.js": "^11.9.0", "ieee754": "^1.2.1", "jimp": "^0.22.12", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 71b311e6..470c828c 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -373,7 +373,8 @@ "Extract EXIF", "Extract ID3", "Extract Files", - "RAKE" + "RAKE", + "Template" ] }, { diff --git a/src/core/operations/Template.mjs b/src/core/operations/Template.mjs new file mode 100644 index 00000000..60b73e1d --- /dev/null +++ b/src/core/operations/Template.mjs @@ -0,0 +1,53 @@ +/** + * @author kendallgoto [k@kgo.to] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Handlebars from "handlebars"; + +/** + * Template operation + */ +class Template extends Operation { + + /** + * Template constructor + */ + constructor() { + super(); + + this.name = "Template"; + this.module = "Handlebars"; + this.description = "Render a template with Handlebars/Mustache substituting variables using JSON input. Templates will be rendered to plain-text only, to prevent XSS."; + this.infoURL = "https://handlebarsjs.com/"; + this.inputType = "JSON"; + this.outputType = "string"; + this.args = [ + { + name: "Template definition (.handlebars)", + type: "text", + value: "" + } + ]; + } + + /** + * @param {JSON} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [templateStr] = args; + try { + const template = Handlebars.compile(templateStr); + return template(input); + } catch (e) { + throw new OperationError(e); + } + } +} + +export default Template; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index bb7016bb..d14f2ff6 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -157,6 +157,7 @@ import "./tests/Subsection.mjs"; import "./tests/SwapCase.mjs"; import "./tests/SymmetricDifference.mjs"; import "./tests/TakeNthBytes.mjs"; +import "./tests/Template.mjs"; import "./tests/TextEncodingBruteForce.mjs"; import "./tests/ToFromInsensitiveRegex.mjs"; import "./tests/TranslateDateTimeFormat.mjs"; diff --git a/tests/operations/tests/Template.mjs b/tests/operations/tests/Template.mjs new file mode 100644 index 00000000..6ee54913 --- /dev/null +++ b/tests/operations/tests/Template.mjs @@ -0,0 +1,53 @@ +/** + * @author kendallgoto [k@kgo.to] + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; +TestRegister.addTests([ + { + "name": "Template: Simple Print", + "input": "{}", + "expectedOutput": "Hello, world!", + "recipeConfig": [ + { + "op": "Template", + "args": ["Hello, world!"] + } + ] + }, + { + "name": "Template: Print Basic Variables", + "input": "{\"one\": 1, \"two\": 2}", + "expectedOutput": "1 2", + "recipeConfig": [ + { + "op": "Template", + "args": ["{{ one }} {{ two }}"] + } + ] + }, + { + "name": "Template: Partials", + "input": "{\"users\":[{\"name\":\"Someone\",\"age\":25},{\"name\":\"Someone Else\",\"age\":32}]}", + "expectedOutput": "Name: Someone\nAge: 25\n\nName: Someone Else\nAge: 32\n\n", + "recipeConfig": [ + { + "op": "Template", + "args": ["{{#*inline \"user\"}}\nName: {{ name }}\nAge: {{ age }}\n{{/inline}}\n{{#each users}}\n{{> user}}\n\n{{/each}}"] + } + ] + }, + { + "name": "Template: Disallow XSS", + "input": "{\"test\": \"\"}", + "expectedOutput": "<script></script>", + "recipeConfig": [ + { + "op": "Template", + "args": ["{{ test }}"] + } + ] + } +]);