summaryrefslogtreecommitdiff
path: root/htroot/js/yacy-markdown.js
blob: 18fd4067682b3afc259d8ea44294f63fc12e96f7 (plain)
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);