// ==UserScript==
// @name 语雀渲染HTML附件
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 拦截 /api/attachments/*/content 接口返回的 JSON 数据,解析并渲染 HTML,方便查看文本的真实效果
// @author SayHeya
// @match https://www.yuque.com/raw?filekey=yuque*
// @grant none
// @run-at document-start
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/533313/%E8%AF%AD%E9%9B%80%E6%B8%B2%E6%9F%93HTML%E9%99%84%E4%BB%B6.user.js
// @updateURL https://update.greasyfork.icu/scripts/533313/%E8%AF%AD%E9%9B%80%E6%B8%B2%E6%9F%93HTML%E9%99%84%E4%BB%B6.meta.js
// ==/UserScript==
(function () {
'use strict';
// 定义标题前缀常量
const TITLE_PREFIX = '[渲染✅]';
/** 判断是否是目标接口 */
const isTargetURL = (url) =>
typeof url === 'string' &&
url.includes('/api/attachments/') &&
url.includes('/content');
/** 判断字符串是否是 HTML 内容 */
function isLikelyHTML(str) {
return typeof str === 'string' && /<[^>]+>/.test(str);
}
/** 设置页面标题前缀 */
function setCustomTitle(prefix) {
const observer = new MutationObserver(() => {
if (!document.title.startsWith(prefix)) {
document.title = prefix + ' ' + document.title.replace(new RegExp(`^${prefix}\\s*`), '');
}
});
observer.observe(document.querySelector('title') || document.head, {
childList: true,
subtree: true,
characterData: true
});
// 初始设置一次
if (document.title) {
document.title = prefix + ' ' + document.title.replace(new RegExp(`^${prefix}\\s*`), '');
} else {
const titleTag = document.createElement('title');
titleTag.textContent = prefix;
document.head.appendChild(titleTag);
}
}
/** 渲染 HTML 内容到页面 */
function renderHTML(htmlContent) {
// 设置标题前缀
setCustomTitle(TITLE_PREFIX);
// 清空页面和样式
document.head.innerHTML = '';
document.body.innerHTML = '';
document.documentElement.style.padding = '0';
document.documentElement.style.margin = '0';
document.documentElement.style.overflow = 'auto';
document.body.style.padding = '0';
document.body.style.margin = '0';
document.body.style.overflow = 'auto';
document.body.style.maxWidth = '100vw';
// 添加基础样式
const style = document.createElement('style');
style.textContent = `
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: auto;
background: #fff;
}
#yuque-rendered-container {
padding: 40px;
max-width: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
`;
document.head.appendChild(style);
// 插入 HTML 内容
const container = document.createElement('div');
container.id = 'yuque-rendered-container';
container.innerHTML = htmlContent;
document.body.appendChild(container);
}
/** 尝试解析并渲染接口内容 */
function tryRenderContent(data) {
const content = data?.data?.content;
if (isLikelyHTML(content)) {
console.log('[✅ 语雀 HTML 内容捕获]');
renderHTML(content);
} else {
console.log('[⛔ 内容不是 HTML]', content);
}
}
/** 拦截 fetch 请求 */
function hookFetch() {
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const [url] = args;
const response = await originalFetch.apply(this, args);
if (isTargetURL(url)) {
const cloned = response.clone();
cloned.json().then(data => {
console.log('[🎯 拦截 fetch]', url, data);
tryRenderContent(data);
}).catch(e => console.warn('❌ fetch JSON 解析失败:', e));
}
return response;
};
}
/** 拦截 XMLHttpRequest 请求 */
function hookXHR() {
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this._intercept_url = url;
return originalOpen.call(this, method, url, ...rest);
};
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (...args) {
this.addEventListener('load', function () {
if (isTargetURL(this._intercept_url)) {
try {
const data = JSON.parse(this.responseText);
console.log('[🎯 拦截 XHR]', this._intercept_url, data);
tryRenderContent(data);
} catch (e) {
console.warn('❌ XHR JSON 解析失败:', e);
}
}
});
return originalSend.apply(this, args);
};
}
// 启动拦截器
hookFetch();
hookXHR();
})();