diff --git a/.gitignore b/.gitignore
index 839130f5b..4f6aa0fc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,12 @@ build/
node_modules/
NOTES
+# KaTeX files
+*.min.css
+KaTeX_**.ttf
+KaTeX_**.woff
+KaTeX_**.woff2
+
# Integration Tests
cypress/screenshots
diff --git a/index.html b/index.html
index 0e52164c1..66baff00b 100644
--- a/index.html
+++ b/index.html
@@ -16,6 +16,7 @@
+
diff --git a/package-lock.json b/package-lock.json
index e9bf16c3f..111d274fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"packages": {
"": {
"version": "1.0.0",
+ "hasInstallScript": true,
"dependencies": {
"@datamodels/identity-profile-basic": "^0.1.2",
"@ethersproject/abstract-provider": "^5.4.0",
@@ -19,8 +20,10 @@
"@types/md5": "^2.3.2",
"@walletconnect/client": "^1.8.0",
"buffer": "^6.0.3",
+ "dompurify": "^2.4.0",
"ethers": "^5.7.1",
"events": "^3.3.0",
+ "katex": "^0.16.2",
"lodash": "^4.17.21",
"lru-cache": "^7.14.0",
"marked": "^4.1.0",
@@ -31,13 +34,14 @@
"siwe": "^2.0.5",
"svelte": "^3.50.1",
"svelte-preprocess": "^4.10.7",
- "svelte-routing": "^1.6.0",
- "xss": "^1.0.14"
+ "svelte-routing": "^1.6.0"
},
"devDependencies": {
"@rsksmart/mock-web3-provider": "^1.0.1",
"@sveltejs/vite-plugin-svelte": "^1.0.8",
"@tsconfig/svelte": "^3.0.0",
+ "@types/dompurify": "^2.3.4",
+ "@types/katex": "^0.14.0",
"@types/lodash": "^4.14.186",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"cypress": "^10.9.0",
@@ -1444,6 +1448,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/dompurify": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.4.tgz",
+ "integrity": "sha512-EXzDatIb5EspL2eb/xPGmaC8pePcTHrkDCONjeisusLFrVfl38Pjea/R0YJGu3k9ZQadSvMqW0WXPI2hEo2Ajg==",
+ "dev": true,
+ "dependencies": {
+ "@types/trusted-types": "*"
+ }
+ },
"node_modules/@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
@@ -1459,6 +1472,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
+ "node_modules/@types/katex": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
+ "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
+ "dev": true
+ },
"node_modules/@types/lodash": {
"version": "4.14.186",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz",
@@ -1527,6 +1546,12 @@
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
+ "dev": true
+ },
"node_modules/@types/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
@@ -2799,11 +2824,6 @@
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
- "node_modules/cssfilter": {
- "version": "0.0.10",
- "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
- "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
- },
"node_modules/cypress": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
@@ -3070,6 +3090,11 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dompurify": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
+ "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
+ },
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -4947,6 +4972,29 @@
"verror": "1.10.0"
}
},
+ "node_modules/katex": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz",
+ "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.0.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keccak": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz",
@@ -7239,26 +7287,6 @@
}
}
},
- "node_modules/xss": {
- "version": "1.0.14",
- "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
- "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
- "dependencies": {
- "commander": "^2.20.3",
- "cssfilter": "0.0.10"
- },
- "bin": {
- "xss": "bin/xss"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/xss/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
- },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -8281,6 +8309,15 @@
"@types/node": "*"
}
},
+ "@types/dompurify": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.4.tgz",
+ "integrity": "sha512-EXzDatIb5EspL2eb/xPGmaC8pePcTHrkDCONjeisusLFrVfl38Pjea/R0YJGu3k9ZQadSvMqW0WXPI2hEo2Ajg==",
+ "dev": true,
+ "requires": {
+ "@types/trusted-types": "*"
+ }
+ },
"@types/form-data": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz",
@@ -8296,6 +8333,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
+ "@types/katex": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz",
+ "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==",
+ "dev": true
+ },
"@types/lodash": {
"version": "4.14.186",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz",
@@ -8364,6 +8407,12 @@
"integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
"dev": true
},
+ "@types/trusted-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
+ "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
+ "dev": true
+ },
"@types/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
@@ -9333,11 +9382,6 @@
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
- "cssfilter": {
- "version": "0.0.10",
- "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
- "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
- },
"cypress": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
@@ -9555,6 +9599,11 @@
"esutils": "^2.0.2"
}
},
+ "dompurify": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
+ "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
+ },
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -10918,6 +10967,21 @@
"verror": "1.10.0"
}
},
+ "katex": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz",
+ "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==",
+ "requires": {
+ "commander": "^8.0.0"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
+ }
+ }
+ },
"keccak": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz",
@@ -12553,22 +12617,6 @@
"integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==",
"requires": {}
},
- "xss": {
- "version": "1.0.14",
- "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
- "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
- "requires": {
- "commander": "^2.20.3",
- "cssfilter": "0.0.10"
- },
- "dependencies": {
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
- }
- }
- },
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
diff --git a/package.json b/package.json
index 3d4153b14..0c14fe364 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"start": "vite",
"serve": "vite preview",
"build": "scripts/build",
+ "postinstall": "scripts/copy-katex-assets",
"check": "scripts/check",
"format": "npx prettier '**/*.@(ts|js|svelte|json|css|html)' --ignore-path .gitignore --write",
"test:unit": "TZ='UTC' vitest run",
@@ -19,6 +20,8 @@
"@rsksmart/mock-web3-provider": "^1.0.1",
"@sveltejs/vite-plugin-svelte": "^1.0.8",
"@tsconfig/svelte": "^3.0.0",
+ "@types/dompurify": "^2.3.4",
+ "@types/katex": "^0.14.0",
"@types/lodash": "^4.14.186",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"cypress": "^10.9.0",
@@ -46,8 +49,10 @@
"@types/md5": "^2.3.2",
"@walletconnect/client": "^1.8.0",
"buffer": "^6.0.3",
+ "dompurify": "^2.4.0",
"ethers": "^5.7.1",
"events": "^3.3.0",
+ "katex": "^0.16.2",
"lodash": "^4.17.21",
"lru-cache": "^7.14.0",
"marked": "^4.1.0",
@@ -58,7 +63,6 @@
"siwe": "^2.0.5",
"svelte": "^3.50.1",
"svelte-preprocess": "^4.10.7",
- "svelte-routing": "^1.6.0",
- "xss": "^1.0.14"
+ "svelte-routing": "^1.6.0"
}
}
diff --git a/scripts/copy-katex-assets b/scripts/copy-katex-assets
new file mode 100755
index 000000000..7c7b114cd
--- /dev/null
+++ b/scripts/copy-katex-assets
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -Eeuo pipefail
+
+cp -r node_modules/katex/dist/katex.min.css public/katex.min.css
+cp -r node_modules/katex/dist/fonts/* public/fonts/
diff --git a/src/Markdown.svelte b/src/Markdown.svelte
index f5d9755c0..d6f02bd1e 100644
--- a/src/Markdown.svelte
+++ b/src/Markdown.svelte
@@ -8,7 +8,7 @@
getImageMime,
isUrl,
} from "@app/utils";
- import xss, { getDefaultWhiteList } from "xss";
+ import dompurify from "dompurify";
export let content: string;
export let getImage: (path: string) => Promise;
@@ -19,18 +19,8 @@
let container: HTMLElement;
- const render = (content: string): string => {
- return xss(marked.parse(content), {
- whiteList: {
- ...getDefaultWhiteList(),
- img: ["src", "alt", "title"],
- audio: ["src"],
- video: ["src"],
- a: ["href", "name"],
- },
- stripIgnoreTag: false,
- });
- };
+ const render = (content: string): string =>
+ dompurify.sanitize(marked.parse(content));
onMount(() => {
// Don't underline tags that contain images.
diff --git a/src/utils.ts b/src/utils.ts
index e993c3cbd..8280129d7 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -3,6 +3,7 @@ import type { RouteLocation } from "@app/index";
import md5 from "md5";
import { BigNumber } from "ethers";
import multibase from "multibase";
+import katex from "katex";
import multihashes from "multihashes";
import type { TransactionResult } from "@gnosis.pm/safe-core-sdk";
import EthersSafe, { EthersAdapter } from "@gnosis.pm/safe-core-sdk";
@@ -808,33 +809,42 @@ export const unreachable = (value: never): never => {
throw new Error(`Unreachable code: ${value}`);
};
-const markdownEmojiTokenizer: marked.TokenizerExtension = {
+const emojisMarkedExtension = {
name: "emoji",
level: "inline",
- start(src: string) {
- return src.indexOf(":");
- },
+ start: (src: string) => src.indexOf(":"),
tokenizer(src: string) {
- const rule = /^:([\w+-]+):/;
- const match = rule.exec(src);
+ const match = src.match(/^:([\w+-]+):/);
if (match) {
return {
type: "emoji",
raw: match[0],
- emoji: match[1],
+ text: match[1].trim(),
};
}
},
+ renderer: (token: marked.Tokens.Generic) =>
+ `${parseEmoji(token.text)}`,
};
-const markdownEmojiRenderer: marked.RendererExtension = {
- name: "emoji",
- renderer(token: marked.Tokens.Generic) {
- return `${parseEmoji(token.emoji)}`;
+const katexMarkedExtension = {
+ name: "katex",
+ level: "inline",
+ start: (src: string) => src.indexOf("$"),
+ tokenizer(src: string) {
+ const match = src.match(/^\$+([^$\n]+?)\$+/);
+ if (match) {
+ return {
+ type: "katex",
+ raw: match[0],
+ text: match[1].trim(),
+ };
+ }
},
+ renderer: (token: marked.Tokens.Generic) =>
+ katex.renderToString(token.text, {
+ throwOnError: false,
+ }),
};
-export const markdownExtensions = [
- markdownEmojiTokenizer,
- markdownEmojiRenderer,
-];
+export const markdownExtensions = [emojisMarkedExtension, katexMarkedExtension];