(() => { 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 `[${label}]`; }; 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')) : ''; } }); })();