mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-21 00:05:20 +00:00
feat(ai): render assistant chat messages as sanitized markdown
The AI chat drawer was rendering assistant responses as plain text, so code blocks, lists, tables and inline formatting came through as literal asterisks and backticks — noisy and hard to scan. Adds a shared renderMarkdown() helper in resources/scripts/utils/ markdown.ts that parses GFM markdown via marked and sanitizes the result with DOMPurify before handing it to Vue's v-html. AiChatMessage uses the helper for assistant messages only; user messages stay as plain text since markdown syntax in their own typed input would be surprising. Assistant bubbles get the Tailwind `prose prose-sm` classes from the already-enabled @tailwindcss/typography plugin so headings, lists and code blocks inherit sensible defaults without per-element styling. Security: DOMPurify runs in its default browser profile, which strips <script>, event handlers, javascript: URLs and every other XSS vector. The AI provider isn't a trusted source — it can echo arbitrary user input and tool-call results from the database — so sanitization is non-negotiable even though the immediate source is our own backend.
This commit is contained in:
42
resources/scripts/utils/markdown.ts
Normal file
42
resources/scripts/utils/markdown.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
* Render a markdown string to safe, sanitized HTML.
|
||||
*
|
||||
* Used by the AI chat drawer to render assistant responses. Even though
|
||||
* the AI provider controls the immediate source of the content, the model
|
||||
* can echo anything it's fed — including user input from earlier in the
|
||||
* conversation or tool results from the database. We therefore parse
|
||||
* markdown → HTML via marked and then sanitize the result with DOMPurify
|
||||
* before handing it to Vue's v-html.
|
||||
*
|
||||
* Marked is configured with:
|
||||
* - gfm: true — GitHub-flavored markdown (tables, fenced code,
|
||||
* strikethrough, task lists). Matches what users
|
||||
* already expect from any modern chat UI.
|
||||
* - breaks: true — newlines become <br> so a single user-typed line
|
||||
* break renders as a visual break without needing
|
||||
* two trailing spaces.
|
||||
* - async: false — force synchronous parsing so the caller doesn't
|
||||
* have to await; marked defaults to returning a
|
||||
* Promise when extensions are registered.
|
||||
*
|
||||
* DOMPurify is run in its default browser profile which strips <script>,
|
||||
* event handlers, javascript: URLs, and every other HTML vector. We do
|
||||
* NOT customize ALLOWED_TAGS because marked's output is already a
|
||||
* conservative subset of HTML.
|
||||
*/
|
||||
export function renderMarkdown(source: string): string {
|
||||
if (!source) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const rawHtml = marked.parse(source, {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
async: false,
|
||||
}) as string
|
||||
|
||||
return DOMPurify.sanitize(rawHtml)
|
||||
}
|
||||
Reference in New Issue
Block a user