summaryrefslogtreecommitdiff
path: root/htroot/js/yacy-markdown.js
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-04 13:52:47 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-04 13:52:47 +0200
commit9f540de0674378ce610594da568925050b6139ec (patch)
tree4f5212d88cb3620c2d43826cc564f436b95257f8 /htroot/js/yacy-markdown.js
parentadb04ae3971ee8f976cc946c943e757a625cb558 (diff)
Enhanced Log Reports
Diffstat (limited to 'htroot/js/yacy-markdown.js')
-rw-r--r--htroot/js/yacy-markdown.js85
1 files changed, 85 insertions, 0 deletions
diff --git a/htroot/js/yacy-markdown.js b/htroot/js/yacy-markdown.js
new file mode 100644
index 000000000..18fd40676
--- /dev/null
+++ b/htroot/js/yacy-markdown.js
@@ -0,0 +1,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);