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
|
isDark?: boolean
|
||||||
size?: "base" | "sm" | "tight"
|
size?: "base" | "sm" | "tight"
|
||||||
disableHighlight?: boolean
|
disableHighlight?: boolean
|
||||||
|
escapeRawHtml?: boolean
|
||||||
onRendered?: () => void
|
onRendered?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +104,12 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
const text = decodeHtmlEntitiesLocally(rawText)
|
const text = decodeHtmlEntitiesLocally(rawText)
|
||||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||||
const highlightEnabled = !props.disableHighlight
|
const highlightEnabled = !props.disableHighlight
|
||||||
|
const escapeRawHtml = Boolean(props.escapeRawHtml)
|
||||||
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
const partId = typeof part.id === "string" && part.id.length > 0 ? part.id : undefined
|
||||||
const cacheId = resolvePartCacheId(part, text)
|
const cacheId = resolvePartCacheId(part, text)
|
||||||
const version = resolvePartVersion(part, text)
|
const version = resolvePartVersion(part, text)
|
||||||
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${version}`
|
const requestKey = `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${escapeRawHtml ? 1 : 0}:${version}`
|
||||||
return { part, text, themeKey, highlightEnabled, partId, cacheId, version, requestKey }
|
return { part, text, themeKey, highlightEnabled, escapeRawHtml, partId, cacheId, version, requestKey }
|
||||||
})
|
})
|
||||||
|
|
||||||
const cacheHandle = useGlobalCache({
|
const cacheHandle = useGlobalCache({
|
||||||
@@ -116,7 +118,7 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
scope: "markdown",
|
scope: "markdown",
|
||||||
cacheId: () => {
|
cacheId: () => {
|
||||||
const { cacheId, themeKey, highlightEnabled } = resolved()
|
const { cacheId, themeKey, highlightEnabled } = resolved()
|
||||||
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}`
|
return `${cacheId}:${themeKey}:${highlightEnabled ? 1 : 0}:${resolved().escapeRawHtml ? 1 : 0}`
|
||||||
},
|
},
|
||||||
version: () => resolved().version,
|
version: () => resolved().version,
|
||||||
})
|
})
|
||||||
@@ -126,7 +128,7 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
text: snapshot.text,
|
text: snapshot.text,
|
||||||
html: renderedHtml,
|
html: renderedHtml,
|
||||||
theme: snapshot.themeKey,
|
theme: snapshot.themeKey,
|
||||||
mode: snapshot.version,
|
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||||
}
|
}
|
||||||
setHtml(renderedHtml)
|
setHtml(renderedHtml)
|
||||||
cacheHandle.set(cacheEntry)
|
cacheHandle.set(cacheEntry)
|
||||||
@@ -138,6 +140,7 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
|
||||||
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
const rendered = await markdown.renderMarkdown(snapshot.text, {
|
||||||
suppressHighlight: !snapshot.highlightEnabled,
|
suppressHighlight: !snapshot.highlightEnabled,
|
||||||
|
escapeRawHtml: snapshot.escapeRawHtml,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (latestRequestKey === snapshot.requestKey) {
|
if (latestRequestKey === snapshot.requestKey) {
|
||||||
@@ -148,10 +151,11 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const snapshot = resolved()
|
const snapshot = resolved()
|
||||||
latestRequestKey = snapshot.requestKey
|
latestRequestKey = snapshot.requestKey
|
||||||
|
const cacheMode = `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`
|
||||||
|
|
||||||
const cacheMatches = (cache: RenderCache | undefined) => {
|
const cacheMatches = (cache: RenderCache | undefined) => {
|
||||||
if (!cache) return false
|
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
|
const localCache = snapshot.part.renderCache
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export default function MessagePart(props: MessagePartProps) {
|
|||||||
sessionId={props.sessionId}
|
sessionId={props.sessionId}
|
||||||
isDark={isDark()}
|
isDark={isDark()}
|
||||||
size={isAssistantMessage() ? "tight" : "base"}
|
size={isAssistantMessage() ? "tight" : "base"}
|
||||||
|
escapeRawHtml={props.messageType === "user"}
|
||||||
onRendered={props.onRendered}
|
onRendered={props.onRendered}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ let highlighterPromise: Promise<Highlighter> | null = null
|
|||||||
let currentTheme: "light" | "dark" = "light"
|
let currentTheme: "light" | "dark" = "light"
|
||||||
let isInitialized = false
|
let isInitialized = false
|
||||||
let highlightSuppressed = false
|
let highlightSuppressed = false
|
||||||
|
let escapeRawHtmlEnabled = false
|
||||||
let rendererSetup = false
|
let rendererSetup = false
|
||||||
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
let shikiModulePromise: Promise<typeof import("shiki/bundle/full")> | null = null
|
||||||
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | 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>`
|
return `<code class="inline-code">${escapeHtml(decoded)}</code>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderer.html = (html: string) => {
|
||||||
|
if (!escapeRawHtmlEnabled) {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapeHtml(decodeHtmlEntities(html))
|
||||||
|
}
|
||||||
|
|
||||||
marked.use({ renderer })
|
marked.use({ renderer })
|
||||||
rendererSetup = true
|
rendererSetup = true
|
||||||
}
|
}
|
||||||
@@ -308,6 +317,7 @@ export async function renderMarkdown(
|
|||||||
content: string,
|
content: string,
|
||||||
options?: {
|
options?: {
|
||||||
suppressHighlight?: boolean
|
suppressHighlight?: boolean
|
||||||
|
escapeRawHtml?: boolean
|
||||||
},
|
},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
@@ -316,6 +326,7 @@ export async function renderMarkdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const suppressHighlight = options?.suppressHighlight ?? false
|
const suppressHighlight = options?.suppressHighlight ?? false
|
||||||
|
const escapeRawHtml = options?.escapeRawHtml ?? false
|
||||||
const decoded = decodeHtmlEntities(content)
|
const decoded = decodeHtmlEntities(content)
|
||||||
|
|
||||||
if (!suppressHighlight) {
|
if (!suppressHighlight) {
|
||||||
@@ -324,13 +335,16 @@ export async function renderMarkdown(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousSuppressed = highlightSuppressed
|
const previousSuppressed = highlightSuppressed
|
||||||
|
const previousEscapeRawHtml = escapeRawHtmlEnabled
|
||||||
highlightSuppressed = suppressHighlight
|
highlightSuppressed = suppressHighlight
|
||||||
|
escapeRawHtmlEnabled = escapeRawHtml
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Proceed to parse immediately - highlighting will be available on next render
|
// Proceed to parse immediately - highlighting will be available on next render
|
||||||
return marked.parse(decoded) as Promise<string>
|
return marked.parse(decoded) as Promise<string>
|
||||||
} finally {
|
} finally {
|
||||||
highlightSuppressed = previousSuppressed
|
highlightSuppressed = previousSuppressed
|
||||||
|
escapeRawHtmlEnabled = previousEscapeRawHtml
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user