Skip to content

Markdown viewer: fix entities, add callouts, line numbers, perf - #4185

Open
cameronsjo wants to merge 7 commits into
manaflow-ai:mainfrom
cameronsjo:feat/md-viewer-improvements-wt
Open

cameronsjo wants to merge 7 commits into
manaflow-ai:mainfrom
cameronsjo:feat/md-viewer-improvements-wt

Conversation

@cameronsjo

@cameronsjo cameronsjo commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

All changes are in Resources/markdown-viewer/shell.html — no Swift changes.

Test plan

  • Open a markdown file with `<div>` and `A & B` — entities should render as literal characters, not &lt;div&gt;
  • Add > [!NOTE], > [!WARNING], etc. blockquotes — should render with colored left border and icon
  • Fenced code blocks should show line numbers in a left gutter; numbers should not be included when copying code
  • Cmd+A should select the full document content, not just one paragraph
  • Rapidly save a file multiple times — viewer should not flicker or double-render

🤖 Generated with Claude Code


Summary by cubic

Upgraded the Markdown viewer with GitHub-style callouts and code-block line numbers, fixed inline-code entities and select-all, and sped up renders with a hash diff, a sanitizer fast-path generated from shared config, and an 80ms debounce. Remote-image privacy is preserved (including srcset/background and http(s) img[src]), callout icons are injected post-sanitization, gutters no longer show phantom lines, and line numbers are excluded from exported HTML and copied text.

  • New Features

    • Callouts: > [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION] with icons and colored borders.
    • Code line numbers: fenced blocks get a non-selectable gutter so copying grabs only code.
  • Bug Fixes

    • Rendering and copy/export: stop double-escaping HTML entities in inline code (marked v13 already escapes); strip one trailing newline to remove phantom last line; exclude line-number gutters from exported HTML and copied text.
    • Security and sanitization: fast-path is built from shared blocklists (strict superset), covers style/media tags, srcset/background, remote http(s) img[src], and quoted/unquoted entity-encoded protocols; callout icons added after sanitization; remote-image privacy gate remains.
    • Input and diffing: Cmd/Ctrl+A selects the entire document across keyboard layouts; FNV‑1a hash uses Math.imul for correct 32‑bit diffing.

Written for commit 8209354. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Code blocks now show per-line gutters (line numbers) and GitHub-style callouts/admonitions (NOTE, TIP, IMPORTANT, WARNING, CAUTION).
  • Bug Fixes
    • Fixed select-all (Cmd+A / Ctrl+A) behavior inside the markdown viewer.
    • Inline code rendering corrected to avoid double-escaping.
  • Refactor
    • Rendering is debounced and uses fast pre-check sanitization to reduce unnecessary re-renders; scroll restored only when content truly changes.
  • Other
    • Exported/copied outputs no longer include line-number gutter elements.

Review Change Stack

@vercel

vercel Bot commented May 14, 2026

Copy link
Copy Markdown

@cameronsjo is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors rendering into doRender(md) with fnv1aHash-based change detection, adds a sanitization regex fast-check, preserves inline-code escaping, renders fenced code with per-line gutters, emits GitHub-style callouts with injected SVG icons, debounces window.__cmuxRenderMarkdown, cleans exported clones of gutters, and fixes Cmd/Ctrl+A selection.

Changes

Markdown Viewer Enhancements

Layer / File(s) Summary
Code gutter and callout CSS
Resources/markdown-viewer/shell.html
Adds CSS for fenced-code line-number gutters and GitHub-style .cmux-callout* styles.
Hash utility
Resources/markdown-viewer/shell.html
Adds fnv1aHash and lastRenderedHash sentinel used for fast rendered-output change detection.
Sanitization fast-path
Resources/markdown-viewer/shell.html
Adds sanitizationPattern and needsSanitization(html) to skip expensive DOM sanitization when output lacks risky patterns.
Codespan and fenced code rendering
Resources/markdown-viewer/shell.html
Updates marked's codespan to emit already-escaped code directly and changes the fenced code renderer to produce a .cmux-code-with-lines flex structure with computed per-line gutter; extends blockquote rendering to detect `[!NOTE
Callout icon injection
Resources/markdown-viewer/shell.html
Adds calloutIcons SVG markup and injectCalloutIcons() to insert icons into .cmux-callout-title elements during post-processing.
Refactored render flow
Resources/markdown-viewer/shell.html
Refactors rendering into doRender(md) (reset slug counts, parse frontmatter, conditional sanitization, compute/update fnv1aHash, capture/restore scroll only on real changes).
Debounce render and export cleanup
Resources/markdown-viewer/shell.html
Debounces window.__cmuxRenderMarkdown(md) with an 80ms timer and immediate first-render path, and removes .cmux-line-gutter from cloned DOM for exported/copied HTML/text.
Select-All keyboard fix
Resources/markdown-viewer/shell.html
Adds a document-level keydown listener that intercepts Cmd+A/Ctrl+A and selects all content inside the markdown viewer root.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant doRender
  participant needsSanitization
  participant DOMPurify
  participant injectCalloutIcons
  participant contentEl

  Client->>doRender: call with md
  doRender->>needsSanitization: test rendered HTML
  alt needs sanitization
    needsSanitization->>DOMPurify: sanitize HTML
    DOMPurify->>doRender: sanitized HTML
  else skip sanitization
    needsSanitization-->>doRender: original HTML
  end
  doRender->>injectCalloutIcons: post-process sanitized HTML
  injectCalloutIcons->>doRender: final HTML
  doRender->>contentEl: set innerHTML (if hash differs)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4288: Overlaps in the markdown rendering and sanitization pipeline changes; both touch exported-DOM cleanup and sanitizer behavior.

Poem

🐰 I hopped through hashes, unescaped each code,

gutters counted lines down every road,
callouts gleamed with icons tucked inside,
renders wait a beat, then smoothly glide,
Cmd+A now gathers all with pride.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux No Hacky Sleeps ❌ Error PR introduces 80ms setTimeout debounce to collapse rapid file-watcher events, violating runtime-no-hacky-sleeps.md which explicitly flags debounce logic using wall-clock delays instead of real events. Replace setTimeout debounce with event-driven approach: use file watcher coalescing, content hash comparison on receipt, or cancellation-aware scheduler instead of fixed 80ms delay.
Cmux Full Internationalization ❌ Error PR adds hardcoded user-facing English text ('Copy', 'Copied', 'Failed', 'Markdown viewer error') in shell.html without routing through localization APIs or updating web/messages/ locale files. Replace hardcoded strings with cmuxString() calls and add corresponding entries to all 20 locales in web/messages/.
Description check ⚠️ Warning The description has a solid summary and test plan, but it is missing required template sections like Demo Video, Review Trigger, and Checklist. Add the missing template sections, especially a demo video for the UI changes plus the review-trigger block and checklist items.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing entity escaping, adding callouts, implementing line numbers, and improving performance—all key features in this changeset.
Linked Issues check ✅ Passed The PR directly addresses both linked issues: #4144 (double-escaping entities in inline code) and #2591 (Cmd+A/Ctrl+A selecting full document instead of paragraph).
Out of Scope Changes check ✅ Passed All changes are scoped to Resources/markdown-viewer/shell.html and directly implement the stated objectives; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed PR contains no Swift code changes; only modifies Resources/markdown-viewer/shell.html (HTML/JavaScript). Swift actor isolation check is not applicable.
Cmux Swift Blocking Runtime ✅ Passed PR only modifies Resources/markdown-viewer/shell.html (HTML/JavaScript), not Swift code. The custom check applies only to production Swift changes, so it does not apply here.
Cmux Swift Concurrency ✅ Passed This PR contains no Swift code modifications—only changes to Resources/markdown-viewer/shell.html (HTML/JavaScript). The custom check for Swift concurrency is not applicable.
Cmux Swift @Concurrent ✅ Passed All async functions in the new Swift files follow the @concurrent annotation rule: network-heavy operations use Task.detached() for explicit actor hops, and UI-bound async work is on @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed PR contains no Swift file changes; all changes are confined to Resources/markdown-viewer/shell.html (HTML). Custom check only applies to production Swift changes.
Cmux Swift Logging ✅ Passed PR modifies only Resources/markdown-viewer/shell.html (HTML/JavaScript), with no Swift code changes; custom check applies only to Swift changes.
Cmux User-Facing Error Privacy ✅ Passed PR modifies only sanitizationPattern regex in Resources/markdown-viewer/shell.html; no user-facing error messages, alerts, or command output are added or changed.
Cmux Swiftui State Layout ✅ Passed PR only modifies Resources/markdown-viewer/shell.html (HTML/JavaScript), not any Swift/SwiftUI files, so the SwiftUI state layout check is not applicable.
Cmux Architecture Rethink ✅ Passed PR modifies only Resources/markdown-viewer/shell.html (HTML/JavaScript); no Swift code changes. The swift-architectural-rethink check applies only to Swift changes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains zero Swift changes—only HTML/JavaScript updates to Resources/markdown-viewer/shell.html. Check is not applicable to non-Swift changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Resources/markdown-viewer/shell.html`:
- Around line 454-460: The fast-path regex in sanitizationPattern used by
needsSanitization is too narrow and can miss unsafe HTML (e.g., "<link>" without
intervening space and event attributes with spaces like "onload ="); update
sanitizationPattern to more robustly detect these cases by changing the
"<link(?:\s)" fragment to "<link\\b" (so tag boundary is respected) and replace
the current on-attribute pattern with something that matches "on" followed by
letters and optional whitespace before the "=" (e.g., "on[a-z]+\\s*=") so
attributes like "onload =" are caught; ensure the revised regex still includes
the existing checks (script, iframe, object, embed, meta, base, form controls,
style/srcdoc/autofocus/formaction/xlink:href and javascript/vbscript/data URI
schemes) and keep it used by needsSanitization and the surrounding
sanitizeRenderedHTML flow.
- Around line 1133-1135: The keyboard shortcut listener using
document.addEventListener('keydown', function(ev) { ... }) should check the
physical key via ev.code instead of the character via ev.key; update the
conditional that currently checks ev.key === 'a' to use ev.code === 'KeyA' while
keeping the modifier checks ((ev.metaKey || ev.ctrlKey) && !ev.shiftKey &&
!ev.altKey) and ev.preventDefault(), so Cmd/Ctrl+A is detected reliably across
layouts and shift states.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 641ac84e-65c5-437b-852d-69c9da9ea06b

📥 Commits

Reviewing files that changed from the base of the PR and between c83f585 and c53e6a9.

📒 Files selected for processing (1)
  • Resources/markdown-viewer/shell.html

Comment thread Resources/markdown-viewer/shell.html
@greptile-apps

greptile-apps Bot commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR upgrades the WebKit markdown viewer with GitHub-style callout blocks, per-line code gutters, a fix for double-escaped HTML entities in inline code (confirmed against the bundled marked v13.0.3), Cmd+A document-level selection, and performance improvements via FNV-1a hash diffing and an 80ms debounce. All changes are confined to Resources/markdown-viewer/shell.html.

  • Callouts: > [!NOTE/TIP/IMPORTANT/WARNING/CAUTION] blockquotes are rendered as styled divs; Octicon SVG icons are injected post-sanitization to survive the sanitizer's blanket <svg> strip.
  • Line numbers: fenced code blocks get a non-selectable gutter; gutters are stripped from exported/copied HTML; the trailing-newline phantom line is fixed.
  • Perf/correctness: null-sentinel (not 0) prevents hash collision on first render; FNV-1a replaces the O(n) innerHTML string diff; needsSanitization fast-path now covers entity-encoded protocols and is derived from the same block list as the sanitizer.

Confidence Score: 4/5

Safe to merge for documents without remote images; callout icons are silently lost whenever the sanitizer runs alongside callout content.

The previously-reported data-cmux-callout-type stripping bug is still present: the sanitizer unconditionally removes all data-cmux-* attributes (lines 1003-1006), so when the full TreeWalker path runs the injectCalloutIcons function finds no matching elements and icons are silently dropped. The four issues flagged in the prior review round are all correctly addressed.

Resources/markdown-viewer/shell.html — the interaction between sanitizeRenderedHTML stripping data-cmux-* attributes and injectCalloutIcons relying on data-cmux-callout-type.

Important Files Changed

Filename Overview
Resources/markdown-viewer/shell.html All changes; callout rendering has a known icon-loss issue when sanitizer runs (previously reported), and multi-paragraph callouts produce a stray empty paragraph in the body.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[__cmuxRenderMarkdown called] --> B{lastRenderedHash === null?}
    B -- Yes, first render --> C[doRender immediately]
    B -- No --> D[clearTimeout + setTimeout 80ms]
    D --> C
    C --> E[extractFrontmatter]
    E --> F[marked.parse body]
    F --> G{needsSanitization regex match?}
    G -- No, fast path --> H[use parsed HTML directly]
    G -- Yes --> I[sanitizeRenderedHTML via TreeWalker]
    I --> J[strips blocked tags, on* attrs, data-cmux-* attrs]
    H --> K[renderFrontmatter + parsed/sanitized]
    J --> K
    K --> L{fnv1aHash === lastRenderedHash?}
    L -- Same hash, skip --> M[rewriteImageSources + postProcessSpecialBlocks]
    L -- Different hash --> N[contentEl.innerHTML = html]
    N --> O[restoreScrollState]
    N --> M
    M --> P[injectCalloutIcons via data-cmux-callout-type]
    P --> Q{attribute present?}
    Q -- Yes, fast path taken --> R[Insert Octicon SVG]
    Q -- No, sanitizer stripped it --> S[Icons silently omitted]
Loading

Reviews (5): Last reviewed commit: "refactor(markdown-viewer): derive saniti..." | Re-trigger Greptile

Comment on lines +613 to +631
blockquote(quote) {
var calloutMatch = (quote || '').match(/^<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i);
if (!calloutMatch) {
return '<blockquote>\n' + quote + '</blockquote>\n';
}
var type = calloutMatch[1].toLowerCase();
var body = quote.replace(calloutMatch[0], '<p>').replace(/^<p>\s*(?:<br\s*\/?>)?\s*/, '<p>');
var icons = {
note: '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>',
tip: '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863.5 8 .5s5.5 1.81 5.5 4.75c0 1.516-.701 2.5-1.328 3.259a10.8 10.8 0 0 0-.268.32c-.207.245-.383.453-.542.68-.207.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/></svg>',
important: '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>',
warning: '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>',
caution: '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>'
};
var title = type.charAt(0).toUpperCase() + type.slice(1);
return '<div class="cmux-callout cmux-callout-' + type + '">'
+ '<div class="cmux-callout-title">' + (icons[type] || '') + ' ' + title + '</div>'
+ '<div class="cmux-callout-body">' + body + '</div>'
+ '</div>\n';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Callout SVGs always stripped by the sanitizer they trigger

The blockquote renderer injects hardcoded <svg viewBox="..."> elements as callout icons. Because needsSanitization includes <svg\b in its trigger pattern, needsSanitization(parsed) returns true for every document that contains a callout. This calls sanitizeRenderedHTML, which has svg: true in blockedTags and unconditionally el.remove()s every <svg> element. The icons are generated and then immediately deleted — they will never be visible in any callout block.

Comment thread Resources/markdown-viewer/shell.html Outdated
Comment on lines +454 to +460
// Fast pre-check: most markdown files produce HTML with no dangerous
// elements or attributes. This regex tests for patterns the full
// TreeWalker sanitizer would act on — if none match, skip the walk.
var sanitizationPattern = /<(?:script|iframe|object|embed|link(?:\s)|meta|base|form|button|textarea|select|option|svg|math)\b|<input\b(?![^>]*type\s*=\s*["']?checkbox)|(?:\son[a-z]+=|\bstyle\s*=|\bsrcdoc\s*=|\bautofocus\b|\bformaction\s*=|\bxlink:href\s*=)|(?:href|src)\s*=\s*["']?\s*(?:javascript|vbscript|data):/i;
function needsSanitization(html) {
return sanitizationPattern.test(html || '');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security needsSanitization regex does not cover HTML-entity-encoded dangerous protocols

The regex checks for the literal string javascript:, vbscript:, or data: in href/src values. HTML-entity-encoded variants such as &#106;avascript: or java&#115;cript: are not matched by the pattern but are decoded by the browser's HTML attribute parser before the URL is evaluated. If such content appears in a markdown-generated link with no other blocked patterns present, the fast-path returns false and the HTML is inserted unsanitized. The full TreeWalker path's isSafeURLAttribute uses compactForProtocolCheck which strips whitespace and control characters, but does not decode HTML entities either — however that path is now skipped entirely for these inputs.

Comment thread Resources/markdown-viewer/shell.html Outdated
Comment on lines +602 to +607
var lineCount = (raw.match(/\n/g) || []).length + 1;
var gutter = '';
for (var ln = 1; ln <= lineCount; ln++) {
gutter += '<span class="cmux-line-num">' + ln + '</span>';
}
return '<pre class="cmux-code-with-lines"><span class="cmux-line-gutter" aria-hidden="true">' + gutter + '</span><code class="hljs' + langClass + '">' + highlighted + '</code></pre>\n';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Extra phantom line number for code blocks ending with a trailing newline

lineCount is computed as (raw.match(/\n/g) || []).length + 1. If raw ends with a \n, the count will be one more than the number of visible lines, producing an empty dangling line number at the bottom of the gutter that has no corresponding code line.

Comment on lines +1077 to 1087
window.__cmuxRenderMarkdown = function(md) {
if (renderTimer) { clearTimeout(renderTimer); }
if (lastRenderedHash === 0) {
doRender(md);
return;
}
renderTimer = setTimeout(function() {
renderTimer = null;
doRender(md);
}, 80);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 lastRenderedHash === 0 used as "not yet rendered" sentinel collides with a valid hash

lastRenderedHash starts at 0 and is used to decide whether to skip the 80ms debounce. FNV-1a on a 32-bit unsigned result can legitimately produce 0 for some inputs. If the hash of the first rendered document is 0, the next call to __cmuxRenderMarkdown will again bypass the debounce. A named sentinel like -1 (which >>> 0 arithmetic never produces) or a separate boolean hasRendered flag would eliminate the ambiguity.

cameronsjo added a commit to cameronsjo/cmux that referenced this pull request May 20, 2026
Addresses the six findings from CodeRabbit and Greptile on PR manaflow-ai#4185:

1. Callout SVGs are no longer stripped by their own sanitizer. The icons
   are kept out of the rendered HTML pipeline and injected from
   postProcessSpecialBlocks() via a new injectCalloutIcons() helper. The
   blockquote renderer emits a data-cmux-callout-type placeholder whose
   value comes from a whitelisted capture group, so the attribute is
   safe even after a sanitization pass. (Greptile P1)

2. The fast-path sanitization regex now triggers on any character
   reference inside an href/src attribute value (e.g. "&manaflow-ai#106;avascript:"),
   so entity-encoded protocol smuggling falls through to the full
   sanitizer (which already decodes via attribute parsing). (Greptile P1
   security)

3. Sanitization regex tightened: "<link\b" replaces "<link(?:\s)" so a
   bare "<link>" is caught, and "\son[a-z]+\s*=" allows whitespace before
   "=", so attributes like "onload =" are detected. (CodeRabbit critical)

4. lastRenderedHash now uses a null sentinel instead of 0. fnv1aHash can
   legitimately produce 0 (FNV-1a 32-bit, post ">>> 0"), which previously
   collided with the "not yet rendered" check. (Greptile P2)

5. Code-block line counting strips exactly one trailing newline before
   counting, removing the phantom dangling line number when raw code
   ends with "\n" (which is the common case from marked). (Greptile P2)

6. The viewer's Cmd/Ctrl+A handler matches on ev.code === "KeyA" instead
   of ev.key === "a" so the shortcut works regardless of keyboard layout
   or shift state. (CodeRabbit minor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

@cameronsjo
cameronsjo force-pushed the feat/md-viewer-improvements-wt branch from 6f428f6 to 32a9c10 Compare May 20, 2026 00:43
cameronsjo added a commit to cameronsjo/cmux that referenced this pull request May 20, 2026
Addresses the six findings from CodeRabbit and Greptile on PR manaflow-ai#4185:

1. Callout SVGs are no longer stripped by their own sanitizer. The icons
   are kept out of the rendered HTML pipeline and injected from
   postProcessSpecialBlocks() via a new injectCalloutIcons() helper. The
   blockquote renderer emits a data-cmux-callout-type placeholder whose
   value comes from a whitelisted capture group, so the attribute is
   safe even after a sanitization pass. (Greptile P1)

2. The fast-path sanitization regex now triggers on any character
   reference inside an href/src attribute value (e.g. "&manaflow-ai#106;avascript:"),
   so entity-encoded protocol smuggling falls through to the full
   sanitizer (which already decodes via attribute parsing). (Greptile P1
   security)

3. Sanitization regex tightened: "<link\b" replaces "<link(?:\s)" so a
   bare "<link>" is caught, and "\son[a-z]+\s*=" allows whitespace before
   "=", so attributes like "onload =" are detected. (CodeRabbit critical)

4. lastRenderedHash now uses a null sentinel instead of 0. fnv1aHash can
   legitimately produce 0 (FNV-1a 32-bit, post ">>> 0"), which previously
   collided with the "not yet rendered" check. (Greptile P2)

5. Code-block line counting strips exactly one trailing newline before
   counting, removing the phantom dangling line number when raw code
   ends with "\n" (which is the common case from marked). (Greptile P2)

6. The viewer's Cmd/Ctrl+A handler matches on ev.code === "KeyA" instead
   of ev.key === "a" so the shortcut works regardless of keyboard layout
   or shift state. (CodeRabbit minor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Resources/markdown-viewer/shell.html`:
- Around line 1091-1097: The rendered line-number spans (class
"cmux-line-gutter") are being serialized into plain-text exports; update the
HTML generation that builds the gutter (the loop creating '<span
class="cmux-line-num">') to mark the gutter container/spans with a removable
data attribute or explicit hook (e.g., add a data-cmux-line-gutter marker on the
gutter element) so it can be identified for stripping, then modify
cleanRenderedContentClone() to remove elements matching that marker (or
'.cmux-line-gutter') during clone cleanup and stop relying on the
[data-cmux-chrome="1"] selector—ensure window.__cmuxRenderedText() uses the
cleaned clone so numbers are omitted from copied/exported plain text.
- Around line 901-926: The fast-path regex in sanitizationPattern (used by
needsSanitization) is missing tags and entity/unquoted cases that
sanitizeRenderedHTML would catch, allowing <style>, media tags
(video/audio/source), unquoted entity-encoded href/src (e.g.
href=&`#106`;avascript:) and similar to bypass sanitization; update
sanitizationPattern to include style and media tags (style, video, audio,
source, picture, track), add unquoted attribute value matching for entity
references on href/src (not just quoted), and ensure input patterns like
\bstyle\b and \b(?:href|src)\s*=\s*[^>\s]*& are covered so needsSanitization
returns true for cases sanitizeRenderedHTML would strip, keeping behavior
consistent with sanitizeRenderedHTML and rewriteLocalImageSources.
- Around line 333-338: The fnv1aHash function uses floating-point multiplication
(hash * 0x01000193) which loses precision; replace that multiplication with
Math.imul to perform a 32-bit integer multiply and preserve correct FNV-1a
behavior (update the multiplication in fnv1aHash to use Math.imul(hash,
0x01000193) and keep the existing >>> 0 unsigned wrap).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1b34298a-d6af-4c51-b433-fb5c8f9d0ff4

📥 Commits

Reviewing files that changed from the base of the PR and between 6f428f6 and 32a9c10.

📒 Files selected for processing (1)
  • Resources/markdown-viewer/shell.html

Comment thread Resources/markdown-viewer/shell.html
Comment thread Resources/markdown-viewer/shell.html Outdated
Comment thread Resources/markdown-viewer/shell.html

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Resources/markdown-viewer/shell.html Outdated
cameronsjo and others added 7 commits July 29, 2026 16:28
…ers, selection

- Fix manaflow-ai#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 manaflow-ai#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) <noreply@anthropic.com>
… debounce

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 <script>, no onclick=, no
  javascript: URLs), so the full DOM walk is skipped entirely on the
  happy path. Sanitizer still fires when suspicious patterns are found.

- Debounce rapid re-renders with 80ms delay. First render is immediate;
  subsequent calls within the window are collapsed. Editors that do
  atomic write-rename (vim, VS Code) fire the file watcher 2-3 times
  in quick succession — this collapses them into one render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses the six findings from CodeRabbit and Greptile on PR manaflow-ai#4185:

1. Callout SVGs are no longer stripped by their own sanitizer. The icons
   are kept out of the rendered HTML pipeline and injected from
   postProcessSpecialBlocks() via a new injectCalloutIcons() helper. The
   blockquote renderer emits a data-cmux-callout-type placeholder whose
   value comes from a whitelisted capture group, so the attribute is
   safe even after a sanitization pass. (Greptile P1)

2. The fast-path sanitization regex now triggers on any character
   reference inside an href/src attribute value (e.g. "&manaflow-ai#106;avascript:"),
   so entity-encoded protocol smuggling falls through to the full
   sanitizer (which already decodes via attribute parsing). (Greptile P1
   security)

3. Sanitization regex tightened: "<link\b" replaces "<link(?:\s)" so a
   bare "<link>" is caught, and "\son[a-z]+\s*=" allows whitespace before
   "=", so attributes like "onload =" are detected. (CodeRabbit critical)

4. lastRenderedHash now uses a null sentinel instead of 0. fnv1aHash can
   legitimately produce 0 (FNV-1a 32-bit, post ">>> 0"), which previously
   collided with the "not yet rendered" check. (Greptile P2)

5. Code-block line counting strips exactly one trailing newline before
   counting, removing the phantom dangling line number when raw code
   ends with "\n" (which is the common case from marked). (Greptile P2)

6. The viewer's Cmd/Ctrl+A handler matches on ev.code === "KeyA" instead
   of ev.key === "a" so the shortcut works regardless of keyboard layout
   or shift state. (CodeRabbit minor)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-path

The render perf work added a needsSanitization() fast-path that skips
sanitizeRenderedHTML() when no dangerous pattern matches. After rebasing
onto main (which landed manaflow-ai#4288's remote-image privacy feature), this
created a gap: manaflow-ai#4288 strips a remote <img> src into data-cmux-remote-src
*inside* sanitizeRenderedHTML — which runs on an inert <template>, so the
image never fetches. A document containing only a plain remote image
matched none of the fast-path patterns, so sanitization was skipped, the
live src survived into the live DOM, and the browser auto-fetched the
remote image — defeating the "don't auto-load remote images" gate.

Add an <img ... src=...http(s): alternative to the fast-path regex so any
remote image forces the full sanitizer. Local/relative and file images
stay on the fast path (rewriteLocalImageSources scans img[src] directly,
independent of sanitization); data: images already trigger via the
existing data: alternative.

Verified against a 22-case regex suite covering the original review
vectors plus remote/local/data image and lazy data-src cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three findings from CodeRabbit's re-review of the rebased branch:

1. FNV-1a hash now uses Math.imul(hash, 0x01000193) instead of
   `hash * 0x01000193`. Plain `*` is an IEEE-754 double multiply; once the
   product exceeds 2^53 the `>>> 0` truncation can't recover the correct
   32-bit value, so the content-change hash could miss real edits. (Major)

2. The needsSanitization() fast-path is now a strict superset of what
   sanitizeRenderedHTML() strips. Added the tags that manaflow-ai#4288 introduced into
   blockedTags but were missing from the regex (style, audio, video,
   picture, source, track), and broadened the entity-reference branch to
   match unquoted href/src values too (e.g. `href=&manaflow-ai#106;avascript:`), so
   nothing the slow path would strip can skip it. (Critical)

3. Line-number gutters (.cmux-line-gutter) are stripped in
   cleanRenderedContentClone(), so exported HTML and copied plain text from
   __cmuxRenderedHTML()/__cmuxRenderedText() no longer carry "1 2 3 …". (Major)

Verified against a 26-case regex suite covering the new tag/entity cases,
the original review vectors, and safe-content negatives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d (round 3)

cubic flagged that needsSanitization() omitted the `srcset` and
`background` attributes that sanitizeRenderedHTML() strips (lines 1001-1002),
violating the documented superset invariant. A document containing only
those attributes (e.g. <img srcset="https://...">) would skip the slow path,
leaving a remote-image privacy bypass. Added \bsrcset\s*= and
\bbackground\s*= to the attribute alternation; the fast-path attribute set
now matches the sanitizer's blocked-attribute list exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three consecutive review rounds flagged the needsSanitization() fast-path
drifting from sanitizeRenderedHTML() — first missing style/media tags, then
unquoted entity refs, then srcset/background. Root cause: the fast-path
regex and the sanitizer's blocked-tag/attr lists were maintained by hand in
two places, so every sanitizer change needed a mirror edit that was easy to
miss.

Make them share one source of truth:

- SANITIZER_BLOCKED_TAGS and SANITIZER_BLOCKED_ATTRS are now module-level
  constants. sanitizeRenderedHTML() consumes them directly (blockedTags
  alias + SANITIZER_BLOCKED_ATTRS.indexOf for attribute stripping).
- buildSanitizationPattern() generates the fast-path regex from the same
  lists, with SANITIZER_BOOLEAN_ATTRS marking valueless attributes
  (autofocus) so they match on a word boundary rather than `=`.
- Adding a tag or attribute now updates both the sanitizer and the
  fast-path together; they cannot drift.

The generated regex is behaviorally identical to the prior hand-written one.
Verified by running the in-file builder against a 30-case suite covering
every blocked tag, blocked attribute (incl. bare boolean autofocus), the
quoted/unquoted entity and active-protocol URL branches, remote vs local
images, and safe-content negatives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cameronsjo
cameronsjo force-pushed the feat/md-viewer-improvements-wt branch from 3e97b56 to 8209354 Compare July 29, 2026 21:35
@cameronsjo

Copy link
Copy Markdown
Contributor Author

Rebased onto current main to clear the merge conflict in Resources/markdown-viewer/shell.html. The branch was a long way behind, and upstream had since touched that file in ~29 commits (callout/octicon CSS, the diff-viewer bundle, the viewer-navigation module).

Both conflict regions were additive on both sides with nothing in the base, so upstream's link() renderer and viewer-navigation install are preserved alongside this PR's blockquote() callouts and Cmd/Ctrl+A handler — nothing was dropped to resolve it. The remaining commits replayed cleanly. Final diff is one file, +239/-38, no conflict markers.

Every feature this PR adds is intact post-rebase, and all of it is still absent from main: GitHub-style callouts, octicon icons, the line-number gutter, FNV-1a content hashing, render debounce, and the sanitizer fast-path hardening.

Prior automated findings (CodeRabbit, greptile, cubic) are all addressed on the rebased head:

  • FNV-1a uses Math.imul for correct 32-bit arithmetic.
  • The sanitizer fast-path is derived from the same tag/attribute lists the full sanitizer uses (buildSanitizationPattern), so the two cannot drift — this covers style/media tags, unquoted entity refs, and srcset/background.
  • Line-number gutter spans are stripped from exported and copied plain text.
  • Cmd/Ctrl+A matches ev.code === 'KeyA', so it survives non-QWERTY layouts.
  • Callout icons are octicon SVGs injected after sanitization via a data-cmux-callout-type placeholder, so the sanitizer's blanket svg strip cannot delete them.

The two greptile threads still showing unresolved were fixed in the first round of fixes and simply never marked resolved on the thread — verified against current source rather than assumed.

Both issues this fixes are still open: #4144 (HTML entities inside backticks) and #2591 (Ctrl+A selecting only one paragraph).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

@cameronsjo I’ll review the rebased PR, including the sanitizer fast-path, rendered-output behavior, and preservation of the upstream viewer integrations.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@teamleaderleo teamleaderleo added S2: major A crash, hang, lost state, broken connection, or a regression on a path people use area: browser The embedded browser, web surfaces, inline VS Code labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: browser The embedded browser, web surfaces, inline VS Code S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ctrl+A in markdown viewer only selects a single paragraph Markdown viewer renders some characters inside backticks as HTML entities

2 participants