diff --git a/package.json b/package.json index c215a70..9e4acf8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/rfc2047": "^2.0.3", "autoprefixer": "^10.4.20", "debug": "^4.3.7", + "dompurify": "^3.2.3", "dotenv": "^16.4.5", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", @@ -45,6 +46,7 @@ "husky": "^9.1.7", "lint-staged": "^15.2.10", "lucide-svelte": "^0.454.0", + "marked": "^15.0.4", "mdsvex": "^0.11.2", "openai": "^4.73.0", "prettier": "^3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6693e23..b16f1c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ importers: debug: specifier: ^4.3.7 version: 4.3.7 + dompurify: + specifier: ^3.2.3 + version: 3.2.3 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -88,6 +91,9 @@ importers: lucide-svelte: specifier: ^0.454.0 version: 0.454.0(svelte@5.2.7) + marked: + specifier: ^15.0.4 + version: 15.0.4 mdsvex: specifier: ^0.11.2 version: 0.11.2(svelte@5.2.7) @@ -2589,6 +2595,12 @@ packages: integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== } + '@types/trusted-types@2.0.7': + resolution: + { + integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + } + '@types/unist@2.0.11': resolution: { @@ -3272,6 +3284,12 @@ packages: integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== } + dompurify@3.2.3: + resolution: + { + integrity: sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA== + } + dotenv@16.4.5: resolution: { @@ -4446,6 +4464,14 @@ packages: integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g== } + marked@15.0.4: + resolution: + { + integrity: sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw== + } + engines: { node: '>= 18' } + hasBin: true + mdsvex@0.11.2: resolution: { @@ -7970,6 +7996,9 @@ snapshots: '@types/tough-cookie@4.0.5': optional: true + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.15.0(jiti@1.21.6))(typescript@5.7.2)': @@ -8380,6 +8409,10 @@ snapshots: dlv@1.1.3: {} + dompurify@3.2.3: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv@16.4.5: {} duplexify@4.1.3: @@ -9240,6 +9273,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + marked@15.0.4: {} + mdsvex@0.11.2(svelte@5.2.7): dependencies: '@types/unist': 2.0.11 diff --git a/src/lib/components/Chatroom.svelte b/src/lib/components/Chatroom.svelte index 9545933..d4624bf 100644 --- a/src/lib/components/Chatroom.svelte +++ b/src/lib/components/Chatroom.svelte @@ -2,6 +2,7 @@ import { Button, Card, Textarea } from 'flowbite-svelte'; import { Mic, Send, Square } from 'lucide-svelte'; // Added Square icon import import AudioPlayer from './AudioPlayer.svelte'; + import { renderMarkdown } from '$lib/utils/renderMarkdown'; interface Conversation { name: string; @@ -96,7 +97,14 @@ {/if}
-

{conv.content}

+

+ {#await renderMarkdown(conv.content)} + Loading ... + {:then content} + + {@html content} + {/await} +

{#if conv.audio} {/if} diff --git a/src/lib/components/session/HostView.svelte b/src/lib/components/session/HostView.svelte index 4b0ec9d..d23798f 100644 --- a/src/lib/components/session/HostView.svelte +++ b/src/lib/components/session/HostView.svelte @@ -16,6 +16,7 @@ import type { Conversation } from '$lib/schema/conversation'; import { X } from 'lucide-svelte'; import { SvelteMap } from 'svelte/reactivity'; + import { renderMarkdown } from '$lib/utils/renderMarkdown'; import ChatHistory from './ChatHistory.svelte'; let { session }: { session: Readable } = $props(); @@ -421,7 +422,14 @@ {#each $session?.resources as resource}

{resource.name}

-

{resource.content}

+

+ {#await renderMarkdown(resource.content)} + Loading ... + {:then content} + + {@html content} + {/await} +

{/each}
diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts index b0f9e8c..69a7798 100644 --- a/src/lib/stores/auth.ts +++ b/src/lib/stores/auth.ts @@ -43,11 +43,11 @@ export async function signInWithGoogle() { } // Sign out function -export async function signOut() { +export async function signOut(f: typeof fetch = fetch) { try { await auth.signOut(); // Clear the session cookie - await fetch('/api/auth/signout', { method: 'POST' }); + await f('/api/auth/signout', { method: 'POST' }); // Redirect to home page await goto('/'); } catch (error) { diff --git a/src/lib/utils/renderMarkdown.ts b/src/lib/utils/renderMarkdown.ts new file mode 100644 index 0000000..3c2cde4 --- /dev/null +++ b/src/lib/utils/renderMarkdown.ts @@ -0,0 +1,22 @@ +import DOMPurify from 'dompurify'; +import { marked } from 'marked'; + +/** + * Renders the given markdown string to HTML and sanitizes it. + * @param markdown - The markdown string to be rendered. + * @returns A promise that resolves to the sanitized HTML string. + */ +export async function renderMarkdown(markdown: string): Promise { + const renderer = new marked.Renderer(); + + // prevent rendering of hidden links + renderer.link = (token) => { + return token.text ? `${token.text} (${token.href})` : token.href; + }; + + // prevent showing images + renderer.image = () => ''; + + const dirty = await marked(markdown, { renderer }); + return DOMPurify.sanitize(dirty); +} diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index c1c2465..33816c6 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -2,11 +2,11 @@ import { signOut } from '$lib/stores/auth'; import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ locals }) => { +export const load: PageServerLoad = async ({ locals, fetch }) => { if (locals.user) { throw redirect(303, '/dashboard'); } else { - signOut(); + signOut(fetch); } return {}; diff --git a/src/routes/template/[id]/ResourceList.svelte b/src/routes/template/[id]/ResourceList.svelte index 53167e6..f2d6509 100644 --- a/src/routes/template/[id]/ResourceList.svelte +++ b/src/routes/template/[id]/ResourceList.svelte @@ -5,6 +5,7 @@ import { page } from '$app/stores'; import type { Template } from '$lib/schema/template'; import { notifications } from '$lib/stores/notifications'; + import { renderMarkdown } from '$lib/utils/renderMarkdown'; // Constants const LIMITS = { @@ -215,10 +216,15 @@

- {resource.content} + {#await renderMarkdown(resource.content)} + {resource.content} + {:then content} + + {@html content} + {/await}

{#if resource.content.length > 100}