feat(web): render remote Markdown images

This commit is contained in:
Yichen Jiang
2026-07-30 20:26:33 +08:00
parent e6a621d17d
commit e55a904e1d
16 changed files with 381 additions and 16 deletions

View File

@@ -224,3 +224,14 @@
color: var(--dsw-alias-label-tertiary);
font-style: italic;
}
.image {
display: block;
width: auto;
max-width: 100%;
height: auto;
margin: 0;
border-radius: 8px;
background: var(--dsw-alias-bg-base);
object-fit: contain;
}

View File

@@ -24,6 +24,15 @@ function sanitizeUrl(url: string): string {
const safeUrl: UrlTransform = url => sanitizeUrl(url)
function remoteImageUrl(url: string): string | undefined {
try {
const protocol = new URL(url).protocol
return protocol === 'http:' || protocol === 'https:' ? url : undefined
} catch {
return undefined
}
}
/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
function buildComponents(streaming: boolean): Components {
return {
@@ -40,7 +49,20 @@ function buildComponents(streaming: boolean): Components {
</a>
)
},
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
img: ({ alt = '', src = '' }) => {
const imageSrc = remoteImageUrl(src)
if (imageSrc === undefined) return <span className={css.imageAlt}>{alt}</span>
return (
<img
className={css.image}
src={imageSrc}
alt={alt}
loading="lazy"
decoding="async"
referrerPolicy="no-referrer"
/>
)
},
table: ({ children }) => (
<div className={css.tableScroll}>
<table>{children}</table>
@@ -74,7 +96,8 @@ const streamingComponents = buildComponents(true)
* Render untrusted assistant-authored Markdown as semantic React elements.
* @param props - Markdown source text preserved by the session projection;
* `streaming` renders fences plain (highlighting lands on the finalize swap).
* @returns A GFM document with raw HTML, relative links, unsafe protocols, and remote images disabled.
* @returns A GFM document with raw HTML, relative destinations, and unsafe
* protocols disabled; absolute HTTP(S) images render directly.
*/
export function MarkdownText({ text, streaming = false }: { text: string; streaming?: boolean }) {
return (