// ==UserScript==
// @name OpenWebUI Force HTML Image Renderer
// @version 1.0.0
// @description OpenWebUI 强制 HTML 图片渲染, 识别 <img> 标签,基于 OpenWebUI v0.6.10 开发。使用前请更改 @match 后面的 URL 以使脚本在你的 OpenWebUI URL中工作。
// @author B3000Kcn
// @match https://your.openwebui.url/*
// @run-at document-idle
// @license MIT
// @namespace https://greasyfork.org/users/1474401
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_VERSION = '1.0.0';
const TARGET_DIV_DEPTH = 22;
// 移除了启动日志,如果需要可以取消下面一行的注释
// console.log(`OpenWebUI 图片强制渲染脚本 v${SCRIPT_VERSION} 已启动。`);
const PROCESSED_CLASS = `html-img-rendered-v1-0-0`; // 建议与脚本版本匹配
const ALLOWED_INNERHTML_MOD_TAGS = ['P', 'SPAN', 'PRE', 'CODE', 'LI', 'BLOCKQUOTE', 'FIGCAPTION', 'LABEL', 'SMALL', 'STRONG', 'EM', 'B', 'I', 'U', 'SUB', 'SUP', 'MARK', 'DEL', 'INS', 'TD', 'TH', 'DT', 'DD'];
const TAGS_TO_SKIP_PROCESSING = ['SCRIPT', 'STYLE', 'HEAD', 'META', 'LINK', 'TITLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'BUTTON', 'SELECT', 'FORM', 'IFRAME', 'VIDEO', 'AUDIO', 'CANVAS', 'SVG', 'IMG'];
const escapedImgTagRegex = /<img\s+([^&]*?)>/gi;
const directImgTagRegex = /<img\s+([^>]*)>/gi;
function getElementDepth(element) {
let depth = 0; let el = element;
while (el) { depth++; el = el.parentElement; }
return depth;
}
function getAttributeValue(attributesString, attributeName) {
if (!attributesString || !attributeName) return null;
try {
const regex = new RegExp(`${attributeName}\\s*=\\s*(["'])(.*?)\\1`, 'i');
const match = attributesString.match(regex);
return match ? match[2] : null;
} catch (e) {
// 在生产版本中,可以选择只在内部处理错误,不向控制台输出,或者保留关键错误输出
// console.error(`${SCRIPT_VERSION} 错误: getAttributeValue 执行失败 for ${attributeName}`, e);
return null;
}
}
function processImageTag(match, attributesStringOriginal, isEscaped) {
let matchPreview = ''; // 主要用于日志,生产版中可以考虑移除或简化
if (typeof match === 'string' && match.length > 0) {
matchPreview = match.substring(0, 60).replace(/\s+/g,' ') + (match.length > 60 ? "..." : "");
} else {
return match; // 如果match无效,直接返回
}
if (!isEscaped && match.startsWith("<img") && match.includes('data-force-rendered="true"')) {
return match;
}
if (!isEscaped && match.startsWith("<div class=\"userscript-image-paragraph\"") && match.includes('data-force-rendered="true"')) {
return match;
}
const attributesString = attributesStringOriginal.trim();
const src = getAttributeValue(attributesString, 'src');
try {
let isSrcInvalid = true;
if (src && typeof src === 'string') {
if (src.toLowerCase().startsWith('javascript:')) {
// console.warn(`警告: 检测到 javascript: SRC 并已阻止.`); // 移除具体日志前缀
} else { isSrcInvalid = false; }
} else {
// if (src !== null || (attributesString && attributesString.includes("src="))) {
// console.warn(`警告: 无效或缺失 src. src: ${src}, 类型: ${typeof src}.`);
// }
}
if (isSrcInvalid) {
return match;
}
let alt, width, height;
try {
alt = getAttributeValue(attributesString, 'alt');
width = getAttributeValue(attributesString, 'width');
height = getAttributeValue(attributesString, 'height');
const img = document.createElement('img');
img.src = src;
img.alt = alt ? alt : `用户HTML图片 (${isEscaped ? '转义' : '直接'}) v${SCRIPT_VERSION}`;
img.setAttribute('data-force-rendered', 'true');
if (width) { if (width.includes('%')) { img.style.width = width; } else { img.setAttribute('width', width.replace(/px$/i, '')); } }
if (height) { if (height.includes('%')) { img.style.height = height; } else { img.setAttribute('height', height.replace(/px$/i, '')); } }
img.style.maxWidth = '100%'; img.style.maxHeight = '300px'; img.style.display = 'block';
img.onerror = function() {
// console.warn(`警告: 图片加载失败: ${this.src}. 将隐藏元素.`); // 移除具体日志前缀
this.style.display = 'none'; // 保留隐藏功能
};
const imgHTML = img.outerHTML;
const pWrappedHTML = `<p class="userscript-image-paragraph" style="margin: 0.5em 0; line-height: normal;">${imgHTML}</p>`;
return pWrappedHTML;
} catch (eInner) {
// console.error(`错误: 在主 try 块中发生错误, 返回原始match.`, eInner);
return match;
}
} catch (eOuter) {
// console.error(`错误: 在最外层 try 块中发生错误, 返回原始match.`, eOuter);
return match;
}
}
function renderImagesInElement(element, forcedByObserver = false) {
if (!element || typeof element.classList === 'undefined' || TAGS_TO_SKIP_PROCESSING.includes(element.tagName.toUpperCase()) || element.isContentEditable) {
return;
}
const isTargetDepthDiv = (element.tagName.toUpperCase() === 'DIV' && getElementDepth(element) === TARGET_DIV_DEPTH);
let isAllowedNonDiv = ALLOWED_INNERHTML_MOD_TAGS.includes(element.tagName.toUpperCase());
if (element.classList.contains(PROCESSED_CLASS)) {
if ( (isTargetDepthDiv && forcedByObserver) || (isAllowedNonDiv && forcedByObserver) ) {
element.classList.remove(PROCESSED_CLASS);
} else { return; }
}
if (typeof element.innerHTML !== 'string') { return; }
let htmlContent = element.innerHTML;
let madeChange = false;
const originalHtmlContent = htmlContent;
const replacerFunc = (type) => (fullMatch, attrs) => {
const newImgHtml = processImageTag(fullMatch, attrs, type === 'escaped');
if (newImgHtml !== fullMatch) { madeChange = true; }
return newImgHtml;
};
escapedImgTagRegex.lastIndex = 0;
if (htmlContent.includes('<img')) {
if (escapedImgTagRegex.test(htmlContent)) {
escapedImgTagRegex.lastIndex = 0;
htmlContent = htmlContent.replace(escapedImgTagRegex, replacerFunc('escaped'));
}
}
directImgTagRegex.lastIndex = 0;
if (htmlContent.includes('<img')) {
if (directImgTagRegex.test(htmlContent)) {
directImgTagRegex.lastIndex = 0;
const tempHtmlContent = htmlContent.replace(directImgTagRegex, replacerFunc('direct'));
if (htmlContent !== tempHtmlContent) { htmlContent = tempHtmlContent; }
}
}
let allowFinalModification = isAllowedNonDiv || isTargetDepthDiv;
if (madeChange) {
if (allowFinalModification) {
// if (isTargetDepthDiv && !isAllowedNonDiv) {
// console.info(`DIV 在目标深度 ${TARGET_DIV_DEPTH},允许修改.`);
// }
if (originalHtmlContent !== htmlContent) {
element.innerHTML = htmlContent;
}
if (!isTargetDepthDiv) {
if (!element.classList.contains(PROCESSED_CLASS)) { element.classList.add(PROCESSED_CLASS); }
}
} else {
if (!isTargetDepthDiv && !element.classList.contains(PROCESSED_CLASS)) { element.classList.add(PROCESSED_CLASS); }
}
} else if (!element.classList.contains(PROCESSED_CLASS) &&
(originalHtmlContent.includes('<img') || originalHtmlContent.includes('<img'))) {
if (!isTargetDepthDiv) { element.classList.add(PROCESSED_CLASS); }
}
}
// 单独、正确地定义 observerCallback, initialScan, activateScript
const observerCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) { renderImagesInElement(node, true);
if (typeof node.querySelectorAll === 'function') {
node.querySelectorAll(ALLOWED_INNERHTML_MOD_TAGS.join(',')).forEach(child => {
if (child.nodeType === Node.ELEMENT_NODE) renderImagesInElement(child, true);
});
node.querySelectorAll('div').forEach(childDiv => {
if (childDiv.nodeType === Node.ELEMENT_NODE && getElementDepth(childDiv) === TARGET_DIV_DEPTH) {
renderImagesInElement(childDiv, true);
}
});
}
}
});
} else if (mutation.type === 'characterData') {
if (mutation.target && mutation.target.parentElement) { renderImagesInElement(mutation.target.parentElement, true); }
}
}
};
const observer = new MutationObserver(observerCallback);
const observerConfig = { childList: true, subtree: true, characterData: true };
function initialScan() {
const allRelevantSelectors = ALLOWED_INNERHTML_MOD_TAGS.join(',') + ',div';
document.querySelectorAll(allRelevantSelectors).forEach(el => {
if (el.offsetParent !== null || document.body.contains(el)) {
renderImagesInElement(el, false);
}
});
}
function activateScript() {
setTimeout(() => {
initialScan();
observer.observe(document.body, observerConfig);
}, 2000);
}
if (document.readyState === 'complete' || document.readyState === 'interactive') { activateScript(); }
else { document.addEventListener('DOMContentLoaded', activateScript, { once: true }); }
})();