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))