fix(renderer): route markdown images through media element

This commit is contained in:
KinomotoMio
2026-07-30 00:25:58 +08:00
parent ff6386c6da
commit c079c50376
3 changed files with 66 additions and 0 deletions
@@ -0,0 +1,50 @@
import { SESSION_IMAGE_TAG } from './session-image-element.js';
const SAFE_IMAGE_PROTOCOLS = new Set(['blob:', 'file:', 'http:', 'https:']);
let configuredMarked = null;
function decodeMarkedAttribute(value) {
const decoder = document.createElement('textarea');
decoder.innerHTML = String(value ?? '');
return decoder.value;
}
function isSafeImageSource(source) {
try {
const url = new URL(source, document.baseURI);
return SAFE_IMAGE_PROTOCOLS.has(url.protocol)
|| (url.protocol === 'data:' && /^data:image\//i.test(source));
} catch {
return false;
}
}
function imageFallback(alt) {
const fallback = document.createElement('span');
fallback.className = 'session-image-fallback';
fallback.textContent = alt || 'Image unavailable';
return fallback.outerHTML;
}
export function renderSessionMarkdownImage(href, title, text) {
const source = decodeMarkedAttribute(href).trim();
const alt = decodeMarkedAttribute(text);
const accessibleTitle = decodeMarkedAttribute(title);
if (!source || !isSafeImageSource(source)) return imageFallback(alt);
const image = document.createElement(SESSION_IMAGE_TAG);
image.setAttribute('src', source);
image.setAttribute('alt', alt);
if (accessibleTitle) image.setAttribute('title', accessibleTitle);
return image.outerHTML;
}
export function configureMarkdownImages(marked) {
if (!marked || configuredMarked === marked) return;
marked.use({
renderer: {
image: renderSessionMarkdownImage,
},
});
configuredMarked = marked;
}
+2
View File
@@ -2,6 +2,7 @@
// Pure helpers with no side-effects on global state (except formatProjectLabel which reads store).
import { state } from './store.js';
import { configureMarkdownImages } from './markdown-image-renderer.js';
// --- Time / formatting ---
@@ -95,6 +96,7 @@ export function highlightTextNodes(rootEl, query) {
export function renderMarkdown(text, opts = {}) {
if (text == null) return '';
// marked is loaded globally via CDN in index.html
configureMarkdownImages(window.marked);
const html = sanitizeMarkdown(window.marked.parse(text));
const cls = opts.variant === 'msg' ? 'markdown-msg'
: opts.variant === 'compact' ? 'markdown-compact'
+14
View File
@@ -363,6 +363,20 @@
.markdown-msg th, .markdown-msg td { border: 1px solid var(--hairline); padding: 5px 9px; text-align: left; }
.markdown-msg th { background: rgba(255,255,255,0.04); font-weight: 600; }
.markdown-msg mark { background: var(--accent-soft); color: var(--accent-2); padding: 0 2px; border-radius: 2px; }
.session-image-fallback {
display: block;
max-width: 100%;
margin: 0.7em 0;
padding: 10px 12px;
overflow: hidden;
border: 1px solid var(--hairline-strong);
border-radius: 6px;
color: var(--muted);
background: rgba(0,0,0,0.22);
font: 12px/1.5 var(--font-mono);
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-section-divider {
display: flex; align-items: center; gap: 10px;