// MarkLite — Markdown Module: marked.js init, sanitizer, rendering, preview links (function () { 'use strict'; var ML = window.MarkLite; // ===== HTML Sanitizer ===== var DANGEROUS_TAGS = new Set(['script', 'iframe', 'object', 'embed', 'applet', 'form', 'base', 'meta', 'link', 'style']); var DANGEROUS_ATTRS = new Set([ 'onabort', 'onanimationend', 'onanimationiteration', 'onanimationstart', 'onauxclick', 'onbeforecopy', 'onbeforecut', 'onbeforepaste', 'onblur', 'oncancel', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'onclose', 'oncontextmenu', 'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'onfocusin', 'onfocusout', 'onfullscreenchange', 'onfullscreenerror', 'ongotpointercapture', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onlostpointercapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpointercancel', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove', 'onpointerout', 'onpointerover', 'onpointerup', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll', 'onsearch', 'onseeked', 'onseeking', 'onselect', 'onselectionchange', 'onselectstart', 'onshow', 'onstalled', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'ontouchcancel', 'ontouchend', 'ontouchmove', 'ontouchstart', 'ontransitionend', 'onvolumechange', 'onwaiting', 'onwheel', 'onanimationend', 'oncontentvisibilityautostatechange', 'onscrollend', 'onscrollsnapchange', 'onscrollsnapchanging' ]); function sanitizeHTML(html) { var doc = new DOMParser().parseFromString(html, 'text/html'); var walker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT); var toRemove = []; while (walker.nextNode()) { var el = walker.currentNode; var tag = el.tagName.toLowerCase(); if (DANGEROUS_TAGS.has(tag)) { toRemove.push(el); continue; } var attrs = Array.from(el.attributes); for (var i = 0; i < attrs.length; i++) { var name = attrs[i].name.toLowerCase(); if (DANGEROUS_ATTRS.has(name) || name.startsWith('on')) el.removeAttribute(attrs[i].name); else if ((name === 'href' || name === 'action' || name === 'formaction' || name === 'xlink:href') && /^\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name); else if (name === 'style' && /expression\s*\(|url\s*\(\s*['"]?\s*javascript:/i.test(attrs[i].value)) el.removeAttribute(attrs[i].name); } } for (var _i = 0; _i < toRemove.length; _i++) toRemove[_i].parentNode.removeChild(toRemove[_i]); return doc.body.innerHTML; } // ===== Rendering ===== ML.renderMarkdown = function (text) { if (typeof marked === 'undefined') { ML.preview.textContent = '错误: Markdown 解析库未加载'; ML.preview.style.color = 'red'; return; } try { var raw = marked.parse(text); ML.preview.innerHTML = sanitizeHTML(raw); ML.preview.style.color = ''; } catch (e) { ML.preview.textContent = '渲染错误: ' + e.message; ML.preview.style.color = 'red'; } }; // ===== Init marked.js ===== ML.initMarked = function () { if (typeof marked === 'undefined') return; var renderer = new marked.Renderer(); renderer.code = function (codeOrToken, legacyLang) { var text = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.text : codeOrToken; var lang = (typeof codeOrToken === 'object' && codeOrToken !== null) ? codeOrToken.lang : legacyLang; var highlighted = text; if (typeof hljs !== 'undefined') { if (lang && hljs.getLanguage(lang)) { try { highlighted = hljs.highlight(text, { language: lang }).value; } catch (e) { /* */ } } else { try { highlighted = hljs.highlightAuto(text).value; } catch (e) { /* */ } } } var langClass = lang ? ' language-' + lang : ''; return '
' + highlighted + '
'; }; renderer.image = function (hrefOrToken, title, text) { var href = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.href : hrefOrToken; var alt = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.text : text; var titleAttr = (typeof hrefOrToken === 'object' && hrefOrToken !== null) ? hrefOrToken.title : title; var src = href; if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:') && !src.startsWith('file://')) { var tab = ML.getActiveTab(); if (tab && tab.filePath) { var dir = tab.filePath.replace(/[/\\][^/\\]+$/, ''); var sep = dir.includes('\\') ? '\\' : '/'; src = 'file://' + (dir + sep + src).replace(/\\/g, '/'); } } var titleStr = titleAttr ? ' title="' + titleAttr + '"' : ''; return '' + alt + ''; }; marked.setOptions({ gfm: true, breaks: true, pedantic: false }); marked.use({ renderer: renderer }); }; // ===== Preview Links ===== ML.initPreviewLinks = function () { ML.preview.addEventListener('click', function (e) { var link = e.target.closest('a'); if (!link) return; e.preventDefault(); var href = link.getAttribute('href'); if (!href) return; if (href.startsWith('#')) { var target = ML.preview.querySelector(href); if (target) target.scrollIntoView({ behavior: 'smooth' }); return; } try { var url = new URL(href); if (url.protocol !== 'http:' && url.protocol !== 'https:') return; } catch (err) { return; } if (typeof window.electronAPI !== 'undefined' && window.electronAPI.openExternal) window.electronAPI.openExternal(href); else window.open(href, '_blank', 'noopener'); }); }; })();