140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
// @ts-nocheck
|
||
import katex from 'katex';
|
||
|
||
function normalizeShowImageSrc(src: string) {
|
||
if (!src) return '';
|
||
const trimmed = src.trim();
|
||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
||
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
||
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
||
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
||
if (idx >= 0) {
|
||
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
||
}
|
||
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
||
return trimmed;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function isSafeImageSrc(src: string) {
|
||
return !!normalizeShowImageSrc(src);
|
||
}
|
||
|
||
function escapeHtml(input: string) {
|
||
return input
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function renderShowImages(root: ParentNode | null = document) {
|
||
if (!root) return;
|
||
// 处理因自闭合解析导致的嵌套:把子 show_image 平铺到父后面
|
||
const nested = Array.from(root.querySelectorAll('show_image show_image')).reverse();
|
||
nested.forEach(child => {
|
||
const parent = child.parentElement;
|
||
if (parent && parent !== root) {
|
||
parent.after(child);
|
||
}
|
||
});
|
||
const nodes = Array.from(root.querySelectorAll('show_image:not([data-rendered])')).reverse();
|
||
nodes.forEach(node => {
|
||
// 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序
|
||
if (node.parentNode && node.firstChild) {
|
||
const parent = node.parentNode;
|
||
const ref = node.nextSibling; // 可能为 null,insertBefore 会当 append
|
||
const children = Array.from(node.childNodes);
|
||
children.forEach(child => parent.insertBefore(child, ref));
|
||
}
|
||
|
||
const rawSrc = node.getAttribute('src') || '';
|
||
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
||
if (!mappedSrc) {
|
||
node.setAttribute('data-rendered', '1');
|
||
node.setAttribute('data-rendered-error', 'invalid-src');
|
||
return;
|
||
}
|
||
const alt = node.getAttribute('alt') || '';
|
||
const safeAlt = escapeHtml(alt.trim());
|
||
const figure = document.createElement('figure');
|
||
figure.className = 'chat-inline-image';
|
||
|
||
const img = document.createElement('img');
|
||
img.loading = 'lazy';
|
||
img.src = mappedSrc;
|
||
img.alt = safeAlt;
|
||
img.onerror = () => {
|
||
figure.classList.add('chat-inline-image--error');
|
||
const tip = document.createElement('div');
|
||
tip.className = 'chat-inline-image__error';
|
||
tip.textContent = '图片加载失败';
|
||
figure.appendChild(tip);
|
||
};
|
||
figure.appendChild(img);
|
||
|
||
if (safeAlt) {
|
||
const caption = document.createElement('figcaption');
|
||
caption.innerHTML = safeAlt;
|
||
figure.appendChild(caption);
|
||
}
|
||
|
||
node.replaceChildren(figure);
|
||
node.setAttribute('data-rendered', '1');
|
||
});
|
||
}
|
||
|
||
let showImageObserver: MutationObserver | null = null;
|
||
|
||
export function setupShowImageObserver() {
|
||
if (showImageObserver) return;
|
||
const container = document.querySelector('.messages-area') || document.body;
|
||
if (!container) return;
|
||
renderShowImages(container);
|
||
showImageObserver = new MutationObserver(() => renderShowImages(container));
|
||
showImageObserver.observe(container, { childList: true, subtree: true });
|
||
}
|
||
|
||
export function teardownShowImageObserver() {
|
||
if (showImageObserver) {
|
||
showImageObserver.disconnect();
|
||
showImageObserver = null;
|
||
}
|
||
}
|
||
|
||
function updateViewportHeightVar() {
|
||
const docEl = document.documentElement;
|
||
const visualViewport = window.visualViewport;
|
||
|
||
if (visualViewport) {
|
||
const vh = visualViewport.height;
|
||
const bottomInset = Math.max(
|
||
0,
|
||
(window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop
|
||
);
|
||
docEl.style.setProperty('--app-viewport', `${vh}px`);
|
||
docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`);
|
||
} else {
|
||
const height = window.innerHeight || docEl.clientHeight;
|
||
if (height) {
|
||
docEl.style.setProperty('--app-viewport', `${height}px`);
|
||
}
|
||
docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)');
|
||
}
|
||
}
|
||
|
||
if (typeof window !== 'undefined') {
|
||
window.katex = katex;
|
||
|
||
updateViewportHeightVar();
|
||
window.addEventListener('resize', updateViewportHeightVar);
|
||
window.addEventListener('orientationchange', updateViewportHeightVar);
|
||
window.addEventListener('pageshow', updateViewportHeightVar);
|
||
if (window.visualViewport) {
|
||
window.visualViewport.addEventListener('resize', updateViewportHeightVar);
|
||
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
|
||
}
|
||
}
|