From 31b328d428b6cb1266aeb890eef052f003379a32 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sun, 15 Feb 2026 16:25:57 -0500 Subject: [PATCH] notifications: handle URL encoding in markdown2html --- quickshell/Common/markdown2html.js | 19 +++++++++++++++---- quickshell/Services/NotificationService.qml | 7 +++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/quickshell/Common/markdown2html.js b/quickshell/Common/markdown2html.js index 23e7567b..04a9b52e 100644 --- a/quickshell/Common/markdown2html.js +++ b/quickshell/Common/markdown2html.js @@ -32,8 +32,15 @@ function markdownToHtml(text) { return `\x00INLINECODE${inlineIndex++}\x00`; }); - // Now process everything else - // Escape HTML entities (but not in code blocks) + // Extract plain URLs before escaping so & in query strings is preserved + const urls = []; + let urlIndex = 0; + html = html.replace(/(^|[\s])((?:https?|file):\/\/[^\s]+)/gm, (match, prefix, url) => { + urls.push(url); + return prefix + `\x00URL${urlIndex++}\x00`; + }); + + // Escape HTML entities (but not in code blocks or URLs) html = html.replace(/&/g, '&') .replace(//g, '>'); @@ -64,8 +71,12 @@ function markdownToHtml(text) { return ''; }); - // Detect plain URLs and wrap them in anchor tags (but not inside existing or markdown links) - html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1$2'); + // Restore extracted URLs as anchor tags (preserves raw & in href) + html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => { + const url = urls[parseInt(index)]; + const display = url.replace(/&/g, '&').replace(//g, '>'); + return `${display}`; + }); // Restore code blocks and inline code BEFORE line break processing html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => { diff --git a/quickshell/Services/NotificationService.qml b/quickshell/Services/NotificationService.qml index 841b77d6..d4beff7c 100644 --- a/quickshell/Services/NotificationService.qml +++ b/quickshell/Services/NotificationService.qml @@ -980,6 +980,13 @@ Singleton { return ""; if (/<\/?[a-z][\s\S]*>/i.test(body)) return body; + + // Decode percent-encoded URLs (e.g. https%3A%2F%2F → https://) + body = body.replace(/\bhttps?%3A%2F%2F[^\s]+/gi, match => { + try { return decodeURIComponent(match); } + catch (e) { return match; } + }); + if (/&(#\d+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]+);/.test(body)) { const decoded = _decodeEntities(body); if (/<\/?[a-z][\s\S]*>/i.test(decoded))