From 8f80db4fc1cde0153e791fafa4ff15d7c75de65c Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Wed, 31 May 2023 23:30:41 +0100 Subject: [PATCH] feat(fronted): add markdown rendering to todo detail view --- frontend/package-lock.json | 74 ++++++++++++++++++- frontend/package.json | 6 +- frontend/src/lib/utils/markdown.ts | 11 +++ frontend/src/routes/todos/[todo]/+page.svelte | 6 +- frontend/src/routes/todos/[todo]/+page.ts | 2 +- frontend/src/routes/users/[user]/+page.svelte | 1 + .../routes/users/[user]/todos/+page.svelte | 5 -- 7 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 frontend/src/lib/utils/markdown.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c4d8750..b2da448 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,12 +8,16 @@ "name": "fastapi-svelte-template", "version": "0.0.1", "dependencies": { + "dompurify": "^3.0.3", "dotenv": "^16.0.3", - "jwt-decode": "^3.1.2" + "jwt-decode": "^3.1.2", + "marked": "^5.0.4" }, "devDependencies": { "@sveltejs/adapter-node": "^1.2.4", "@sveltejs/kit": "^1.5.0", + "@types/dompurify": "^3.0.2", + "@types/marked": "^5.0.0", "autoprefixer": "^10.4.14", "postcss": "^8.4.23", "sass": "^1.62.1", @@ -2028,12 +2032,27 @@ "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", "dev": true }, + "node_modules/@types/dompurify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz", + "integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/marked": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz", + "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==", + "dev": true + }, "node_modules/@types/pug": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", @@ -2046,6 +2065,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", + "dev": true + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -2371,6 +2396,11 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dompurify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz", + "integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==" + }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -2767,6 +2797,17 @@ "node": ">=12" } }, + "node_modules/marked": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.4.tgz", + "integrity": "sha512-r0W8/DK56fAkV0qfUCO9cEt/VlFWUzoJOqEigvijmsVkTuPOHckh7ZutNJepRO1AxHhK96/9txonHg4bWd/aLA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5654,12 +5695,27 @@ "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", "dev": true }, + "@types/dompurify": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.2.tgz", + "integrity": "sha512-YBL4ziFebbbfQfH5mlC+QTJsvh0oJUrWbmxKMyEdL7emlHJqGR2Qb34TEFKj+VCayBvjKy3xczMFNhugThUsfQ==", + "dev": true, + "requires": { + "@types/trusted-types": "*" + } + }, "@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "@types/marked": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz", + "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==", + "dev": true + }, "@types/pug": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", @@ -5672,6 +5728,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", + "dev": true + }, "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -5880,6 +5942,11 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "dompurify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz", + "integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ==" + }, "dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -6192,6 +6259,11 @@ "@jridgewell/sourcemap-codec": "^1.4.13" } }, + "marked": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.4.tgz", + "integrity": "sha512-r0W8/DK56fAkV0qfUCO9cEt/VlFWUzoJOqEigvijmsVkTuPOHckh7ZutNJepRO1AxHhK96/9txonHg4bWd/aLA==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index be9b7ea..57479f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,8 @@ "devDependencies": { "@sveltejs/adapter-node": "^1.2.4", "@sveltejs/kit": "^1.5.0", + "@types/dompurify": "^3.0.2", + "@types/marked": "^5.0.0", "autoprefixer": "^10.4.14", "postcss": "^8.4.23", "sass": "^1.62.1", @@ -26,7 +28,9 @@ }, "type": "module", "dependencies": { + "dompurify": "^3.0.3", "dotenv": "^16.0.3", - "jwt-decode": "^3.1.2" + "jwt-decode": "^3.1.2", + "marked": "^5.0.4" } } diff --git a/frontend/src/lib/utils/markdown.ts b/frontend/src/lib/utils/markdown.ts new file mode 100644 index 0000000..7917553 --- /dev/null +++ b/frontend/src/lib/utils/markdown.ts @@ -0,0 +1,11 @@ +import { marked } from 'marked'; +import dompurify from 'dompurify'; + +export function markdownToHtml(text: string): string { + // Remove zerowidth chars from beginning of input string + // This is probably not necessary... + text = text.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, ""); + + // Return markdown parsed as HTML and subsequently sanitized + return dompurify.sanitize(marked.parse(text)); +} diff --git a/frontend/src/routes/todos/[todo]/+page.svelte b/frontend/src/routes/todos/[todo]/+page.svelte index 2efc4b3..9b59df5 100644 --- a/frontend/src/routes/todos/[todo]/+page.svelte +++ b/frontend/src/routes/todos/[todo]/+page.svelte @@ -1,7 +1,11 @@

{data.todo.title}

+
+ {@html markdownToHtml(data.todo.description)} +
diff --git a/frontend/src/routes/todos/[todo]/+page.ts b/frontend/src/routes/todos/[todo]/+page.ts index 19495da..58c0709 100644 --- a/frontend/src/routes/todos/[todo]/+page.ts +++ b/frontend/src/routes/todos/[todo]/+page.ts @@ -13,6 +13,6 @@ export const load = (async ({ params }) => { }) satisfies PageLoad; -export interface UserProfilePage { +export interface TodoDetailPage { todo: TodoItem } diff --git a/frontend/src/routes/users/[user]/+page.svelte b/frontend/src/routes/users/[user]/+page.svelte index 1c8cc9c..86d2ff0 100644 --- a/frontend/src/routes/users/[user]/+page.svelte +++ b/frontend/src/routes/users/[user]/+page.svelte @@ -5,3 +5,4 @@

Profile page for {data.user.email}

+{data.user.first_name}'s Todos diff --git a/frontend/src/routes/users/[user]/todos/+page.svelte b/frontend/src/routes/users/[user]/todos/+page.svelte index 7ce2b67..d4d871c 100644 --- a/frontend/src/routes/users/[user]/todos/+page.svelte +++ b/frontend/src/routes/users/[user]/todos/+page.svelte @@ -17,11 +17,6 @@ isLink: true, linkTarget: '/todos/%id%', }, - { - field: "description", - heading: "Description", - sortable: false, - }, { field: "done", heading: "Done",