diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json
index cb480d3..710f20d 100644
--- a/app/frontend/package-lock.json
+++ b/app/frontend/package-lock.json
@@ -11,7 +11,9 @@
"@fontsource/roboto": "^5.2.9",
"@mdi/font": "^7.4.47",
"buffer": "^6.0.3",
+ "dompurify": "^3.3.1",
"irc-framework": "^4.14.0",
+ "markdown-it": "^14.1.0",
"pinia": "^3.0.4",
"vite-plugin-node-polyfills": "^0.25.0",
"vue": "^3.2.37",
@@ -20,6 +22,8 @@
"devDependencies": {
"@babel/types": "^7.18.10",
"@tsconfig/node22": "^22.0.5",
+ "@types/dompurify": "^3.0.5",
+ "@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
@@ -1569,6 +1573,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/dompurify": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1583,6 +1597,38 @@
"license": "MIT",
"peer": true
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
@@ -2127,7 +2173,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true,
"license": "Python-2.0"
},
"node_modules/asn1.js": {
@@ -2999,6 +3044,15 @@
"url": "https://bevry.me/fund"
}
},
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4542,6 +4596,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
"node_modules/local-pkg": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
@@ -4597,6 +4660,35 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4617,6 +4709,12 @@
"safe-buffer": "^5.1.2"
}
},
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
"node_modules/middleware-handler": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/middleware-handler/-/middleware-handler-0.2.0.tgz",
@@ -5308,6 +5406,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@@ -6535,6 +6642,12 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/ufo": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
diff --git a/app/frontend/package.json b/app/frontend/package.json
index 6f572d7..f3ac2bd 100644
--- a/app/frontend/package.json
+++ b/app/frontend/package.json
@@ -12,7 +12,9 @@
"@fontsource/roboto": "^5.2.9",
"@mdi/font": "^7.4.47",
"buffer": "^6.0.3",
+ "dompurify": "^3.3.1",
"irc-framework": "^4.14.0",
+ "markdown-it": "^14.1.0",
"pinia": "^3.0.4",
"vite-plugin-node-polyfills": "^0.25.0",
"vue": "^3.2.37",
@@ -21,6 +23,8 @@
"devDependencies": {
"@babel/types": "^7.18.10",
"@tsconfig/node22": "^22.0.5",
+ "@types/dompurify": "^3.0.5",
+ "@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
diff --git a/app/frontend/src/components/MessageList.vue b/app/frontend/src/components/MessageList.vue
index 47f1210..bbb132d 100644
--- a/app/frontend/src/components/MessageList.vue
+++ b/app/frontend/src/components/MessageList.vue
@@ -61,9 +61,8 @@ watch(
>
{{ msg.message }}
-
+
diff --git a/app/frontend/src/stores/irc.ts b/app/frontend/src/stores/irc.ts
index 43c9c5b..43f5e27 100644
--- a/app/frontend/src/stores/irc.ts
+++ b/app/frontend/src/stores/irc.ts
@@ -4,6 +4,8 @@ import { Client } from "irc-framework";
import { useBufferStore } from "./bufferStore";
import { ref } from "vue";
import { useAccountStore } from "./accountStore";
+import MarkdownIt from "markdown-it";
+import DOMPurify from "dompurify";
export type HookFunction = (event: any) => HookStatus;
@@ -19,6 +21,39 @@ interface Batch {
params: string[];
}
+const md = new MarkdownIt({
+ html: false,
+ linkify: true,
+ typographer: true,
+});
+
+function renderMessage(msgIn: string) {
+ if (!msgIn) return "";
+ const dirty = md.render(msgIn);
+ return DOMPurify.sanitize(dirty, {
+ ALLOWED_TAGS: [
+ "li",
+ "ul",
+ "ol",
+ "b",
+ "i",
+ "em",
+ "strong",
+ "a",
+ "code",
+ "pre",
+ "br",
+ "p",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ ],
+ ALLOWED_ATTR: ["href", "target", "class"],
+ });
+}
+
export const useIRCStore = defineStore("ircStore", () => {
const bufferStore = useBufferStore();
const accountStore = useAccountStore();
@@ -289,6 +324,7 @@ export const useIRCStore = defineStore("ircStore", () => {
});
}
+ message.message = renderMessage(message.message);
buffer.messages.push(message);
if (