fix(ui): escape raw HTML in user prompt messages (#260)
## Summary - escape raw HTML when rendering user message markdown so prompt input is shown as text instead of injected HTML - keep assistant and tool markdown behavior unchanged by scoping the escape behavior to user messages - update markdown cache keys so escaped and non-escaped render output do not collide ## Verification - `npm run typecheck --workspace @codenomad/ui` *(fails in this workspace because frontend dependencies are not installed)* - `npm run build --workspace @codenomad/ui` *(fails in this workspace because `vite` is not installed)* -- Yours, [CodeNomadBot](https://github.com/NeuralNomadsAI/CodeNomad) Co-authored-by: Shantur <shantur@Mac.home>
This commit is contained in:
committed by
GitHub
parent
37b3f85e61
commit
d1a27ac31b
@@ -83,6 +83,7 @@ interface MarkdownProps {
|
||||
isDark?: boolean
|
||||
size?: "base" | "sm" | "tight"
|
||||
disableHighlight?: boolean
|
||||
escapeRawHtml?: boolean
|
||||
onRendered?: () => void
|
||||
}
|
||||
|
||||
@@ -103,11 +104,12 @@ export function Markdown(props: MarkdownProps) {
|
||||
const text = decodeHtmlEntitiesLocally(rawText)
|
||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||
const highlightEnabled = !props.disableHighlight
|
||||
const escapeRawHtml = Boolean(props.escapeRawHtml)
|
||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
||||
const cacheId = resolvePartCacheId(part, text)
|
||||
const version = resolvePartVersion(part, text)
|
||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${version}`
|
||||
return { part, text, themeKey, highlightEnabled, partId, cacheId, version, requestKey }
|
||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${version}`
|
||||
return { part, text, themeKey, highlightEnabled, escapeRawHtml, partId, cacheId, version, requestKey }
|
||||
})
|
||||
|
||||
const cacheHandle = useGlobalCache({
|
||||
@@ -116,7 +118,7 @@ export function Markdown(props: MarkdownProps) {
|
||||
scope: "markdown",
|
||||
cacheId: () => {
|
||||
const { cacheId, themeKey, highlightEnabled } = resolved()
|
||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${resolved().escapeRawHtml ? 1 : 0}`
|
||||
},
|
||||
version: () => resolved().version,
|
||||
})
|
||||
@@ -126,7 +128,7 @@ export function Markdown(props: MarkdownProps) {
|
||||
text: snapshot.text,
|
||||
html: renderedHtml,
|
||||
theme: snapshot.themeKey,
|
||||
mode: snapshot.version,
|
||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||
}
|
||||
setHtml(renderedHtml)
|
||||
cacheHandle.set(cacheEntry)
|
||||
@@ -138,6 +140,7 @@ export function Markdown(props: MarkdownProps) {
|
||||
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
||||
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
||||
suppressHighlight: !snapshot.highlightEnabled,
|
||||
escapeRawHtml: snapshot.escapeRawHtml,
|
||||
})
|
||||
|
||||
if (latestRequestKey === snapshot.requestKey) {
|
||||
@@ -148,10 +151,11 @@ export function Markdown(props: MarkdownProps) {
|
||||
createEffect(() => {
|
||||
const snapshot = resolved()
|
||||
latestRequestKey = snapshot.requestKey
|
||||
const cacheMode = `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`
|
||||
|
||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||
if (!cache) return false
|
||||
return cache.theme === snapshot.themeKey && cache.mode === snapshot.version
|
||||
return cache.theme === snapshot.themeKey && cache.mode === cacheMode
|
||||
}
|
||||
|
||||
const localCache = snapshot.part.renderCache
|
||||
|
||||
@@ -146,6 +146,7 @@ export default function MessagePart(props: MessagePartProps) {
|
||||
sessionId={props.sessionId}
|
||||
isDark={isDark()}
|
||||
size={isAssistantMessage() ? "tight" : "base"}
|
||||
escapeRawHtml={props.messageType === "user"}
|
||||
onRendered={props.onRendered}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -11,6 +11,7 @@ let highlighterPromise: Promise<Highlighter> | null = null
|
||||
let currentTheme: "light" | "dark" = "light"
|
||||
let isInitialized = false
|
||||
let highlightSuppressed = false
|
||||
let escapeRawHtmlEnabled = false
|
||||
let rendererSetup = false
|
||||
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
|
||||
@@ -285,6 +286,14 @@ function setupRenderer(isDark: boolean) {
|
||||
return `<code class="inline-code">${escapeHtml(decoded)}</code>`
|
||||
}
|
||||
|
||||
renderer.html = (html: string) => {
|
||||
if (!escapeRawHtmlEnabled) {
|
||||
return html
|
||||
}
|
||||
|
||||
return escapeHtml(decodeHtmlEntities(html))
|
||||
}
|
||||
|
||||
marked.use({ renderer })
|
||||
rendererSetup = true
|
||||
}
|
||||
@@ -308,6 +317,7 @@ export async function renderMarkdown(
|
||||
content: string,
|
||||
options?: {
|
||||
suppressHighlight?: boolean
|
||||
escapeRawHtml?: boolean
|
||||
},
|
||||
): Promise<string> {
|
||||
if (!isInitialized) {
|
||||
@@ -316,6 +326,7 @@ export async function renderMarkdown(
|
||||
}
|
||||
|
||||
const suppressHighlight = options?.suppressHighlight ?? false
|
||||
const escapeRawHtml = options?.escapeRawHtml ?? false
|
||||
const decoded = decodeHtmlEntities(content)
|
||||
|
||||
if (!suppressHighlight) {
|
||||
@@ -324,13 +335,16 @@ export async function renderMarkdown(
|
||||
}
|
||||
|
||||
const previousSuppressed = highlightSuppressed
|
||||
const previousEscapeRawHtml = escapeRawHtmlEnabled
|
||||
highlightSuppressed = suppressHighlight
|
||||
escapeRawHtmlEnabled = escapeRawHtml
|
||||
|
||||
try {
|
||||
// Proceed to parse immediately - highlighting will be available on next render
|
||||
return marked.parse(decoded) as Promise<string>
|
||||
} finally {
|
||||
highlightSuppressed = previousSuppressed
|
||||
escapeRawHtmlEnabled = previousEscapeRawHtml
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user