From 1f24528350a958815ff12953932cc3cdbec93473 Mon Sep 17 00:00:00 2001 From: Cameron Sjo Date: Thu, 14 May 2026 18:31:09 -0500 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20improve=20markdown=20viewer=20?= =?UTF-8?q?=E2=80=94=20fix=20entities,=20add=20callouts,=20line=20numbers,?= =?UTF-8?q?=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix #4144: stop double-escaping HTML entities in inline code spans. marked v13 passes codespan content already escaped; remove redundant escapeHtml() call and add unescapeHtml() for path detection. - Add GitHub-style callout/admonition blocks (> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION]) with colored left borders and Octicon SVG icons. - Add line number gutter to fenced code blocks. Numbers are non-selectable so copy grabs only code content. - Fix #2591: Cmd+A / Ctrl+A now selects the entire document content instead of just the focused paragraph. Co-Authored-By: Claude Opus 4.6 (1M context) --- Resources/markdown-viewer/shell.html | 105 ++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/Resources/markdown-viewer/shell.html b/Resources/markdown-viewer/shell.html index c4f0bd7553bf..b5f1e6f36442 100644 --- a/Resources/markdown-viewer/shell.html +++ b/Resources/markdown-viewer/shell.html @@ -50,6 +50,33 @@ padding: 0; /* hljs class already pads */ overflow: auto; } +/* Line number gutter for fenced code blocks. */ +pre.cmux-code-with-lines { + display: flex; + flex-direction: row; +} +pre.cmux-code-with-lines code.hljs { + flex: 1; + min-width: 0; +} +.cmux-line-gutter { + display: flex; + flex-direction: column; + padding: 16px 0; + padding-right: 12px; + padding-left: 12px; + border-right: 1px solid var(--borderColor-muted); + text-align: right; + -webkit-user-select: none; + user-select: none; + flex-shrink: 0; +} +.cmux-line-num { + font-size: 12px; + line-height: 1.5; + color: var(--fgColor-muted); + opacity: 0.55; +} .cmux-code-block { position: relative; margin: 0 0 16px 0; @@ -225,6 +252,39 @@ margin: 0.6em 0; text-align: left; } +/* GitHub-style callout/admonition blocks. */ +.cmux-callout { + padding: 12px 16px; + margin: 0 0 16px 0; + border-left: 4px solid; + border-radius: 6px; + background: var(--bgColor-muted); +} +.cmux-callout-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 14px; + margin-bottom: 6px; +} +.cmux-callout-title svg { + flex-shrink: 0; + width: 16px; + height: 16px; +} +.cmux-callout-body > :first-child { margin-top: 0; } +.cmux-callout-body > :last-child { margin-bottom: 0; } +.cmux-callout-note { border-left-color: #539bf5; } +.cmux-callout-tip { border-left-color: #3fb950; } +.cmux-callout-important { border-left-color: #a371f7; } +.cmux-callout-warning { border-left-color: #d29922; } +.cmux-callout-caution { border-left-color: #f85149; } +.cmux-callout-note .cmux-callout-title { color: #539bf5; } +.cmux-callout-tip .cmux-callout-title { color: #3fb950; } +.cmux-callout-important .cmux-callout-title { color: #a371f7; } +.cmux-callout-warning .cmux-callout-title { color: #d29922; } +.cmux-callout-caution .cmux-callout-title { color: #f85149; } /* Custom selection color that reads on both themes. */ ::selection { background: rgba(56, 139, 253, 0.4); } /* Smooth-anchor scroll for heading links. */ @@ -1012,8 +1072,9 @@ pedantic: false, renderer: { codespan(code) { - var raw = code || ''; - return '' + escapeHtml(raw) + ''; + // marked v13 passes code already HTML-escaped; don't double-escape. + // Markdown-file links are detected later in markMarkdownFileLinks(). + return '' + (code || '') + ''; }, code(code, infostring, escaped) { var raw = code || ''; @@ -1044,7 +1105,12 @@ highlighted = escapeHtml(raw); } var langClass = langName ? ' language-' + langName : ''; - return '
' + highlighted + '
\n'; + var lineCount = (raw.match(/\n/g) || []).length + 1; + var gutter = ''; + for (var ln = 1; ln <= lineCount; ln++) { + gutter += '' + ln + ''; + } + return '
' + highlighted + '
\n'; }, heading(text, level, raw) { var slug = uniqueHeadingSlug(raw); @@ -1056,6 +1122,26 @@ html += ' title="' + escapeAttribute(decodeHTML(title)) + '"'; } return html + '>' + sanitizeLinkLabelHTML(text) + ''; + }, + blockquote(quote) { + var calloutMatch = (quote || '').match(/^

\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i); + if (!calloutMatch) { + return '

\n' + quote + '
\n'; + } + var type = calloutMatch[1].toLowerCase(); + var body = quote.replace(calloutMatch[0], '

').replace(/^

\s*(?:)?\s*/, '

'); + var icons = { + note: '', + tip: '', + important: '', + warning: '', + caution: '' + }; + var title = type.charAt(0).toUpperCase() + type.slice(1); + return '

' + + '
' + (icons[type] || '') + ' ' + title + '
' + + '
' + body + '
' + + '
\n'; } } }); @@ -1825,6 +1911,19 @@ target: document, getScroller: markdownScroller }); + + // Fix Cmd+A / Ctrl+A to select the entire document content rather + // than just the focused paragraph (WebKit default behavior). + document.addEventListener('keydown', function(ev) { + if ((ev.metaKey || ev.ctrlKey) && !ev.shiftKey && !ev.altKey && ev.key === 'a') { + ev.preventDefault(); + var range = document.createRange(); + range.selectNodeContents(contentEl); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + }); })(); From 291d4be25268acae5103b826ce5bb6d5d1f0cb18 Mon Sep 17 00:00:00 2001 From: Cameron Sjo Date: Thu, 14 May 2026 18:36:38 -0500 Subject: [PATCH 2/7] =?UTF-8?q?perf:=20optimize=20markdown=20render=20path?= =?UTF-8?q?=20=E2=80=94=20hash=20diff,=20sanitizer=20fast-path,=20debounce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three performance improvements to the markdown viewer render cycle: - Replace O(n) innerHTML string comparison with FNV-1a hash check. On a 200KB document, this cuts the "nothing changed" path from milliseconds of string comparison to microseconds of integer compare. - Add regex pre-check before the sanitizer TreeWalker. Most markdown files produce no dangerous HTML (no