fix(editor): rewrite local images in markdown runs of the split renderer

Every .md preview renders through MarkdownDocument (htmlInfo is ungated),
but its markdown segments passed the raw source into MarkdownText, whose
chat-security stance drops non-http(s) image destinations — so local
markdown-syntax images (![alt](./icon.svg), reference-style definitions)
always fell back to alt text. Only HTML <img src> tags were rewritten
(postProcessSanitized).

Apply rewriteLocalImageUrls to each markdown run (segment + appended
reference definitions) inside MarkdownDocument's prepared memo, through
the same media trust fence the sanitized HTML leaves already use.
Idempotent for already-absolute media URLs; remote destinations
untouched. Also correct the stale "non-null only for HTML documents"
comment on TextEditor's htmlInfo.
This commit is contained in:
Menghuan1918
2026-09-06 21:53:49 +08:00
parent bb21657eae
commit 2a99968fa3
3 changed files with 64 additions and 8 deletions
+11 -5
View File
@@ -10,9 +10,10 @@
* Security posture: every HTML string (block leaves, inline text, wrapper
* open-tag attributes) goes through DOMPurify with an explicit denylist on
* top of its defaults (no script/style/iframe/forms), anchors are forced to
* open in a new tab with noopener, and local media `src` attributes are
* rewritten through the session-scoped `/sidebar/file` media route — the same
* trust fence the markdown image rewriter (`markdown-images.ts`) uses.
* open in a new tab with noopener, and local media goes through the
* session-scoped `/sidebar/file` media route — markdown-syntax image
* destinations are rewritten via `markdown-images.ts` and `src` attributes
* through `resolveLocalMediaDest` (the same trust fence).
*/
import { useLayoutEffect, useMemo, useRef } from 'react'
import { createElement, type ReactNode } from 'react'
@@ -21,7 +22,7 @@ import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ComponentType } from 'react'
import { markdownTextProps } from './markdown-labels.tsx'
import { lazyChunkComponent } from './lazy-chunk.tsx'
import { resolveLocalMediaDest } from './markdown-images.ts'
import { resolveLocalMediaDest, rewriteLocalImageUrls } from './markdown-images.ts'
import {
analyzeHtmlSegment,
type AnalyzedMarkdownHtml,
@@ -235,7 +236,12 @@ export function MarkdownDocument({ info, media, codeLabels }: MarkdownDocumentPr
const prepared = useMemo<PreparedSegment[]>(() => info.segments.map((segment): PreparedSegment => {
if (segment.kind === 'markdown') {
const defs = info.referenceDefinitions
const text = defs === '' ? segment.text : `${segment.text}\n\n${defs}`
const raw = defs === '' ? segment.text : `${segment.text}\n\n${defs}`
// MarkdownText drops non-http(s) image destinations (chat-security
// stance), so rewrite local ones into /sidebar/file media URLs first —
// the same trust fence the sanitized HTML leaves below go through.
// Idempotent: already-absolute media URLs pass through untouched.
const text = rewriteLocalImageUrls(raw, media.scope, media.path, media.origin)
return {
kind: 'markdown',
text,
+5 -3
View File
@@ -346,9 +346,11 @@ export function TextEditor(props: FileViewerProps) {
() => (markdown && mode === 'preview' ? splitMermaidBlocks(previewMdText) : []),
[markdown, mode, previewMdText],
)
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null only
* for documents that actually contain HTML — plain markdown keeps the
* legacy single-pass render path below, byte-for-byte. */
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null for
* every markdown preview, so the render below always takes the split
* renderer — its markdown runs rewrite local image destinations internally
* (see MarkdownHtml.tsx). The legacy single-pass branches (fed the
* pre-rewritten `previewText`) are dead in the current wiring. */
const htmlInfo = useMemo(
() => (markdown && mode === 'preview' ? analyzeMarkdownHtml(previewMdText) : null),
[markdown, mode, previewMdText],