diff --git a/extensions/ShowierData9978/html.js b/extensions/ShowierData9978/html.js
new file mode 100644
index 0000000000..6684e57f6e
--- /dev/null
+++ b/extensions/ShowierData9978/html.js
@@ -0,0 +1,260 @@
+// Name: HTWL
+// ID: ShowierTWHtml
+// Description: Allows for building HTML within scratch.
+// By: ShowierData9978
+/* eslint-disable require-await */
+///
+
+/**
+ * @typedef {import("@turbowarp/scratch-vm/")}
+
+ */
+
+((Scratch) => {
+ "use strict";
+
+ if (!Scratch.extensions.unsandboxed) {
+ throw new Error("HTML Extension must be run unsandboxed");
+ }
+
+ /**
+ * @type {VM}
+ */
+ const vm = Scratch.vm;
+
+ class Html {
+ constructor() {
+ /**
+ * @typedef {Object.>} stack
+ * @typedef {Object.} html
+ */
+
+ /** @type {stack} */
+ this.stack = {};
+ /** @type {html} */
+ this.html = {};
+ }
+
+ /**
+ *
+ * @returns {Scratch.Info}
+ */
+ getInfo() {
+ return {
+ id: "HTWL",
+ name: "HTML",
+ color1: "#FF0000",
+ blocks: [
+ {
+ opcode: "htmlWrap",
+ blockType: Scratch.BlockType.CONDITIONAL,
+ text: "<[element] [attributes]>",
+ arguments: {
+ element: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "div",
+ },
+ attributes: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "",
+ },
+ },
+ },
+ {
+ opcode: "htmlCommand",
+ blockType: Scratch.BlockType.COMMAND,
+ text: "<[element] [attributes]>[text] >",
+ arguments: {
+ element: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "div",
+ },
+ attributes: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "",
+ },
+ text: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "",
+ },
+ },
+ },
+ {
+ opcode: "rawInsert",
+ blockType: Scratch.BlockType.COMMAND,
+ text: "Insert raw [html]",
+ arguments: {
+ html: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "",
+ },
+ },
+ },
+ {
+ opcode: "noInner",
+ blockType: Scratch.BlockType.COMMAND,
+ text: "<[element] [attributes] />",
+ arguments: {
+ element: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "div",
+ },
+ attributes: {
+ type: Scratch.ArgumentType.STRING,
+ defaultValue: "",
+ },
+ },
+ },
+
+ "---",
+ {
+ opcode: "exit",
+ blockType: Scratch.BlockType.COMMAND,
+ text: ">",
+ },
+ {
+ opcode: "html_ret",
+ blockType: Scratch.BlockType.REPORTER,
+ text: "html",
+ },
+ {
+ opcode: "clear",
+ blockType: Scratch.BlockType.COMMAND,
+ text: "reset html",
+ },
+ ],
+ };
+ }
+
+ sanitise(text) {
+ return text.replace(//g, ">");
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ *
+ */
+ pushStack(element, util) {
+ if (!this.stack[util.target.id]) this.stack[util.target.id] = [];
+
+ this.stack[util.target.id].push(element);
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ * @returns {string}
+ */
+
+ popStack(util) {
+ if (!this.stack[util.target.id]) {
+ this.stack[util.target.id] = [];
+ return;
+ }
+
+ return this.stack[util.target.id].pop();
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ getStack(util) {
+ if (!this.stack[util.target.id]) {
+ this.stack[util.target.id] = [];
+ }
+
+ return this.stack[util.target.id];
+ }
+
+ /**
+ * @typedef arg_wrap
+ * @prop {string} element
+ * @prop {string} attributes
+ * @param {arg_wrap} args
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ async htmlWrap({ element, attributes }, util) {
+ this._appendHtml(
+ `<${this.sanitise(element)} ${this.sanitise(attributes)}>`,
+ util
+ );
+ this.pushStack(element, util);
+ return true;
+ }
+
+ /**
+ * @param {arg_wrap} args
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ async noInner({ element, attributes }, util) {
+ this._appendHtml(
+ `<${this.sanitise(element)} ${this.sanitise(attributes)} />`,
+ util
+ );
+ return true;
+ }
+
+ /**
+ * @typedef arg_command
+ * @prop {string} element
+ * @prop {string} attributes
+ * @prop {string} text
+ * @param {arg_command} args
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ async htmlCommand({ element, attributes, text }, util) {
+ element = this.sanitise(element);
+ attributes = this.sanitise(attributes);
+
+ this._appendHtml(`<${element} ${attributes}>${text}${element}>`, util);
+ return true;
+ }
+
+ /**
+ * @param {string} text
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ _appendHtml(text, util) {
+ let whitespace = " ".repeat(
+ this.stack.length ? this.getStack(util).length - 1 : 0
+ );
+ if (this.html[util.target.id] === undefined) {
+ this.html[util.target.id] = "";
+ }
+ this.html[util.target.id] += whitespace + text + "\n";
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ exit(args, util) {
+ /* @type {string} */
+ const element = this.popStack(util);
+ if (!element) {
+ throw "Error: No element to close";
+ }
+ this._appendHtml(`${this.sanitise(element)}>`, util);
+ }
+
+ rawInsert({ html }, util) {
+ this._appendHtml(html, util);
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ * @returns {string}
+ */
+ html_ret(args, util) {
+ return this.html[util.target.id];
+ }
+
+ /**
+ * @param {import("scratch-vm").BlockUtility} util
+ */
+ clear(args, util) {
+ this.html[util.target.id] = "";
+ }
+ }
+
+ Scratch.extensions.register(new Html());
+ // @ts-ignore
+})(Scratch);
diff --git a/extensions/extensions.json b/extensions/extensions.json
index 06b95e22b9..6b33b69024 100644
--- a/extensions/extensions.json
+++ b/extensions/extensions.json
@@ -83,5 +83,6 @@
"itchio",
"gamejolt",
"obviousAlexC/newgroundsIO",
- "Lily/McUtils"
+ "Lily/McUtils",
+ "ShowierData9978/html"
]
diff --git a/images/ShowierData9978/html.svg b/images/ShowierData9978/html.svg
new file mode 100644
index 0000000000..4b5045a0ff
--- /dev/null
+++ b/images/ShowierData9978/html.svg
@@ -0,0 +1,26 @@
+
diff --git a/images/ShowierData9978/source.fig b/images/ShowierData9978/source.fig
new file mode 100644
index 0000000000..3fa310d8c5
Binary files /dev/null and b/images/ShowierData9978/source.fig differ