Files
MarkLite/renderer/markdown.js
T

104 lines
6.0 KiB
JavaScript

// 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 '<pre><code class="hljs' + langClass + '">' + highlighted + '</code></pre>';
};
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 '<img src="' + src + '" alt="' + alt + '"' + titleStr + '>';
};
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');
});
};
})();