1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
/*
* yacy-markdown.js
* Shared markdown rendering helpers, used by yacychat.html and LogReports_p.html.
* Renders LLM-generated markdown to sanitized HTML: marked for parsing, a
* sanitizer that strips active content (the source is model output, not trusted
* input), and highlight.js for fenced code blocks. Style rules for the rendered
* output live in env/markdown.css, scoped to the .markdown-body class.
* Requires js/marked.umd.js; js/highlight.min.js is optional.
*/
(function (global) {
"use strict";
const markedOptions = {
gfm: true,
breaks: true,
smartLists: true,
mangle: false,
headerIds: false
};
function escapeHTML(value) {
const div = document.createElement('div');
div.textContent = value || '';
return div.innerHTML;
}
function sanitizeHTML(html) {
if (!html) return '';
const template = document.createElement('template');
template.innerHTML = html;
const blockedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta']);
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT, null);
const toRemove = [];
while (walker.nextNode()) {
const el = walker.currentNode;
if (!el || !el.tagName) continue;
const tag = el.tagName.toLowerCase();
if (blockedTags.has(tag)) {
toRemove.push(el);
continue;
}
for (const attr of Array.from(el.attributes)) {
const name = attr.name.toLowerCase();
const value = attr.value || '';
if (name.startsWith('on')) {
el.removeAttribute(attr.name);
continue;
}
if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) {
el.removeAttribute(attr.name);
}
}
}
toRemove.forEach(node => node.remove());
return template.innerHTML;
}
/**
* Render markdown source to sanitized HTML; falls back to escaped plain text
* when marked is not available or parsing fails.
* optionOverrides can adjust the shared marked options per call, e.g.
* { breaks: false } for documents with hard-wrapped source lines where a
* single newline must not become a visible line break.
*/
function render(source, optionOverrides) {
const text = typeof source === 'string' ? source : '';
if (typeof marked === 'undefined') return escapeHTML(text);
try {
marked.setOptions(Object.assign({}, markedOptions, optionOverrides || {}));
return sanitizeHTML(marked.parse(text));
} catch (err) {
console.warn('Markdown rendering failed', err);
return escapeHTML(text);
}
}
/** Apply highlight.js to all fenced code blocks below the given container. */
function highlightAll(container) {
if (!container || typeof hljs === 'undefined') return;
hljs.configure({ ignoreUnescapedHTML: true });
container.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
}
global.YaCyMarkdown = { markedOptions, escapeHTML, sanitizeHTML, render, highlightAll };
})(window);
|