From f2e1cdda59b6cbd3bb8a1963b1fe6d55f4d93f40 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 4 May 2026 14:03:47 +0200 Subject: [PATCH] Implement markdown parsing and improve link handling for summary display --- ui/renderer.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/ui/renderer.js b/ui/renderer.js index 3f2b3ed..f62a2ca 100644 --- a/ui/renderer.js +++ b/ui/renderer.js @@ -3,6 +3,7 @@ const invoke = tauriApi?.core?.invoke; const listen = tauriApi?.event?.listen; const convertFileSrc = tauriApi?.core?.convertFileSrc; const confirmDialog = tauriApi?.dialog?.confirm; +const MarkdownIt = window.markdownit; const DEFAULT_MASTER_PROMPT = `You are an expert summarizer. Summarize the following video concisely: Title: {title} @@ -30,6 +31,78 @@ if (!invoke || !listen) { throw new Error('Tauri runtime API is unavailable.'); } +if (typeof MarkdownIt !== 'function') { + throw new Error('markdown-it is unavailable.'); +} + +const markdownRenderer = createMarkdownRenderer(); + +function createMarkdownRenderer() { + const renderer = new MarkdownIt({ + html: false, + linkify: true, + typographer: false, + breaks: true + }); + const defaultLinkOpen = + renderer.renderer.rules.link_open || + ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options)); + + renderer.renderer.rules.link_open = (tokens, idx, options, env, self) => { + tokens[idx].attrJoin('class', 'summary-link'); + tokens[idx].attrSet('target', '_blank'); + tokens[idx].attrSet('rel', 'noopener noreferrer'); + return defaultLinkOpen(tokens, idx, options, env, self); + }; + + return renderer; +} + +function preprocessLLMMarkdown(text) { + let normalized = String(text || '') + .replace(/<\/think(?:ing)?>[^\S\n]*\n+[^\S\n]*/gi, '') + .replace( + /(^|\n)\s*[\s\S]*?(?:<\/think(?:ing)?>|$)\s*(\n\s*\n)?/gi, + (_, lead) => (lead ? '\n' : '') + ) + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/[\u00a0\u202f\u2007]/g, ' '); + + return balanceStreamingCodeFence(normalized); +} + +function markdownToHTML(text) { + return markdownRenderer.render(preprocessLLMMarkdown(text)); +} + +function balanceStreamingCodeFence(markdown) { + const lines = markdown.split(/\r?\n/); + let open = null; + + for (const line of lines) { + if (!open) { + const match = line.match(/^\s*([`~]{3,})([^\s]*)?.*$/); + if (match) { + open = { fenceChar: match[1][0], fenceLen: match[1].length }; + } + continue; + } + + const closeFence = new RegExp(`^\\s*(${open.fenceChar}{${open.fenceLen},})\\s*$`); + if (closeFence.test(line)) { + open = null; + } + } + + if (!open) { + return markdown; + } + + const closingFence = open.fenceChar.repeat(open.fenceLen); + return markdown.endsWith('\n') ? markdown + closingFence : `${markdown}\n${closingFence}`; +} + function toWebviewFileUrl(filePath) { if (!filePath) { return filePath; @@ -420,14 +493,31 @@ window.addEventListener('DOMContentLoaded', async () => { summaryHTML.appendChild(missingMsg); } if (!expanded) { + summaryHTML.style.display = '-webkit-box'; summaryHTML.style.webkitLineClamp = '2'; summaryHTML.style.maxHeight = '2.8em'; } else { + summaryHTML.style.display = 'block'; summaryHTML.style.webkitLineClamp = ''; summaryHTML.style.maxHeight = ''; } } + summaryHTML.addEventListener('click', (event) => { + const link = event.target.closest('a[href]'); + if (!link || !summaryHTML.contains(link)) { + return; + } + const href = link.getAttribute('href'); + if (!href || href.startsWith('#')) { + return; + } + event.preventDefault(); + window.api.openExternal(href).catch(err => { + console.error('Failed to open summary link:', err); + }); + }); + middle.appendChild(summaryHTML); entry.appendChild(left);