From b3dc603b944cbfe073e7e2bf5cd126017367d7ae Mon Sep 17 00:00:00 2001 From: Tyler <68524461+TySP-Dev@users.noreply.github.com> Date: Sun, 17 May 2026 16:02:31 -0400 Subject: [PATCH] Better markdown support --- ollama_answers.py | 95 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/ollama_answers.py b/ollama_answers.py index e62a1bc..a8b48a6 100644 --- a/ollama_answers.py +++ b/ollama_answers.py @@ -408,6 +408,22 @@ INTERACTIVE_CSS = ''' padding-left: 0.5rem; color: var(--color-base-font, #cdd6f4); } + .sxng-md-content { + line-height: 1.6; + } + .sxng-md-content ul, .sxng-md-content ol { + margin: 0.5rem 0; + padding-left: 1.5rem; + } + .sxng-md-content li { + margin: 0.2rem 0; + } + .sxng-md-content p { + margin: 0.4rem 0; + } + .sxng-md-content code { + font-family: monospace; + } ''' INTERACTIVE_HTML = ''' @@ -430,6 +446,62 @@ INTERACTIVE_HTML = ''' ''' CITATION_HELPER_JS = r''' + function parseMarkdown(text) { + text = text.replace(/\*\*(.*?)\*\*/g, '$1'); + text = text.replace(/__(.*?)__/g, '$1'); + text = text.replace(/(?$1'); + text = text.replace(/(?$1'); + text = text.replace(/`([^`]+)`/g, '$1'); + text = text.replace(/((?:^|\n)[*\-+] .+)+/g, (match) => { + const items = match.trim().split('\n').map(line => { + const content = line.replace(/^[*\-+] /, '').trim(); + return `
  • ${content}
  • `; + }).join(''); + return ``; + }); + text = text.replace(/((?:^|\n)\d+\. .+)+/g, (match) => { + const items = match.trim().split('\n').map(line => { + const content = line.replace(/^\d+\. /, '').trim(); + return `
  • ${content}
  • `; + }).join(''); + return `
      ${items}
    `; + }); + text = text.replace(/^### (.+)$/gm, '

    $1

    '); + text = text.replace(/^## (.+)$/gm, '

    $1

    '); + text = text.replace(/^# (.+)$/gm, '

    $1

    '); + text = text.replace(/^---+$/gm, '
    '); + text = text.replace(/\n\n/g, '

    '); + text = text.replace(/\n(?!<)/g, '
    '); + return text; + } + + function linkCitationsInElement(el, urls) { + const walker = document.createTreeWalker( + el, NodeFilter.SHOW_TEXT, null + ); + const textNodes = []; + let node; + while (node = walker.nextNode()) { + textNodes.push(node); + } + textNodes.forEach(textNode => { + const text = textNode.textContent; + if (!/\[\d/.test(text)) return; + const span = document.createElement('span'); + span.innerHTML = text.replace(/\[(\d{1,2}(?:,\s*\d{1,2})*)\]/g, (match, nums) => { + return nums.split(/\s*,\s*/).map(n => { + const idx = parseInt(n.trim()); + const url = urls[idx - 1]; + if (url) { + return `[${n.trim()}]`; + } + return match; + }).join(''); + }); + textNode.parentNode.replaceChild(span, textNode); + }); + } + function renderCitations(text, urls) { const fragment = document.createDocumentFragment(); const re = /\[(\d{1,2}(?:\s*,\s*\d{1,2})*)\]/g; @@ -1039,17 +1111,19 @@ FRONTEND_JS_TEMPLATE = r""" if (cursor) cursor.remove(); - let last = data.lastChild; - while (last) { - if (last.textContent && last.textContent.trim().length === 0) { - const prev = last.previousSibling; - last.remove(); - last = prev; - } else { - if (last.textContent) last.textContent = last.textContent.trimEnd(); - break; + // Replace streamed text nodes with markdown-rendered content + Array.from(data.childNodes).forEach(node => { + if (node.nodeType !== 1 || !node.classList.contains('sxng-prior-history')) { + node.remove(); } - } + }); + + const rendered = parseMarkdown(fullText.trim()); + const mdDiv = document.createElement('div'); + mdDiv.className = 'sxng-md-content'; + mdDiv.innerHTML = rendered; + linkCitationsInElement(mdDiv, urls); + data.appendChild(mdDiv); renderCitationFooter(fullText, urls, data); @@ -1546,6 +1620,7 @@ class SXNGPlugin(Plugin): "Answer the question directly using the provided context.", "MUST CITE SOURCES by tailing a sentence with [n] or [n,n] etc. If citing general knowledge, use [*].", "Never explain your process. The user expects a direct response.", + "Use markdown formatting where it improves clarity: **bold** for key terms, bullet lists for enumerations, numbered lists for steps. Keep formatting minimal and purposeful.", intent_cfg['format'], "If sources and general knowledge are insufficient, respond with 'Insufficient information to answer.'" ]