新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件, 批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、 图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文, 页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。 便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化, 避免升级后 data/ 被遗留在旧目录。 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
97 lines
2.9 KiB
JavaScript
97 lines
2.9 KiB
JavaScript
(() => {
|
|
const MarkdownIt = window.markdownit;
|
|
const purifier = window.DOMPurify;
|
|
const MAX_MARKDOWN_LENGTH = 256 * 1024;
|
|
|
|
function safeExternalUrl(value) {
|
|
const raw = String(value || '').trim();
|
|
if (!/^https?:\/\//i.test(raw)) return '';
|
|
try {
|
|
const url = new URL(raw);
|
|
if (!/^https?:$/.test(url.protocol) || url.username || url.password) return '';
|
|
return url.toString();
|
|
} catch (error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
if (typeof MarkdownIt !== 'function' || !purifier || typeof purifier.sanitize !== 'function') {
|
|
window.AiMarkdown = Object.freeze({
|
|
available: false,
|
|
mount(root, source) {
|
|
root.classList.add('ai-output-plain');
|
|
root.textContent = String(source || '');
|
|
},
|
|
externalUrl() {
|
|
return '';
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const markdown = new MarkdownIt({
|
|
html: false,
|
|
breaks: true,
|
|
linkify: true,
|
|
typographer: false
|
|
});
|
|
|
|
markdown.renderer.rules.link_open = (tokens, index, options, env, renderer) => {
|
|
const token = tokens[index];
|
|
const url = safeExternalUrl(token.attrGet('href'));
|
|
token.attrSet('href', '#');
|
|
if (url) {
|
|
token.attrSet('data-external-url', url);
|
|
token.attrSet('rel', 'noopener noreferrer');
|
|
} else {
|
|
token.attrJoin('class', 'ai-md-link-blocked');
|
|
token.attrSet('aria-disabled', 'true');
|
|
}
|
|
return renderer.renderToken(tokens, index, options);
|
|
};
|
|
|
|
markdown.renderer.rules.image = (tokens, index) => {
|
|
const alt = markdown.utils.escapeHtml(String(tokens[index].content || '').trim());
|
|
const label = alt ? `图片:${alt}` : '外部图片已阻止';
|
|
return `<span class="ai-md-image-placeholder" role="note">[${label}]</span>`;
|
|
};
|
|
|
|
const sanitizeOptions = Object.freeze({
|
|
ALLOWED_TAGS: [
|
|
'p', 'br', 'strong', 'em', 's', 'blockquote', 'pre', 'code',
|
|
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'a', 'span'
|
|
],
|
|
ALLOWED_ATTR: [
|
|
'href', 'title', 'class', 'rel', 'role', 'aria-disabled', 'data-external-url'
|
|
],
|
|
ALLOW_DATA_ATTR: true,
|
|
ALLOW_ARIA_ATTR: true
|
|
});
|
|
|
|
function render(source) {
|
|
return purifier.sanitize(markdown.render(String(source || '')), sanitizeOptions);
|
|
}
|
|
|
|
window.AiMarkdown = Object.freeze({
|
|
available: true,
|
|
render,
|
|
mount(root, source) {
|
|
const text = String(source || '');
|
|
if (text.length > MAX_MARKDOWN_LENGTH) {
|
|
root.classList.add('ai-output-plain');
|
|
root.textContent = text;
|
|
return;
|
|
}
|
|
root.classList.remove('ai-output-plain');
|
|
root.innerHTML = render(text);
|
|
},
|
|
externalUrl(target) {
|
|
const link = target && typeof target.closest === 'function'
|
|
? target.closest('a[data-external-url]')
|
|
: null;
|
|
return link ? safeExternalUrl(link.getAttribute('data-external-url')) : '';
|
|
}
|
|
});
|
|
})();
|