feat(markdown): GitHub-style rendering for markdown preview - #1037
feat(markdown): GitHub-style rendering for markdown preview#1037Gimics wants to merge 11 commits into
Conversation
📝 WalkthroughWalkthroughAdds a static Markdown preview pipeline with frontmatter parsing, sanitization, GitHub-style alerts, heading anchors, local images, table directives, filesystem refresh handling, themed styling, and a comprehensive demo document with focused tests. ChangesMarkdown preview pipeline
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
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. Comment |
|
@crynta posted as draft because of the roadmap's comment about "Preview surface expansion (better image / Markdown handling)" and having to add a couple of dependencies (though they were already included in an existing package). But these changes are pretty impactful, at least for me, with all of my ai-facing markdown files ... so I'd love to see this make it's way to main. |
b20c8f4 to
bda7495
Compare
Markdown files render the way GitHub renders them, built entirely on the app's existing Streamdown renderer with a rebuilt rehype pipeline: raw, table layout directives, GitHub alerts, heading anchors, local image resolution, sanitize, harden (order is load-bearing; sanitize prunes everything the plugins inject). - GitHub styling: vendored github-markdown-css mapped onto Terax theme tokens, so custom themes keep working - Frontmatter renders as a table, like GitHub - Table layout directives: an HTML comment above a table controls width and column sizing, invisible on GitHub - GFM alerts, all five types, icons as CSS mask data URIs so the sanitizer stays closed to svg - Heading anchors with GitHub-slug ids; TOC links scroll within the pane instead of navigating - Relative images resolve against the document directory and load zero-copy through the asset protocol, the same path the editor uses - The preview re-reads when the file changes on disk (agents, git, scripts) and keeps scroll position; wired to the existing fs watcher - Rendering is fully static: no streaming repair, one parse, a 7k-line document stays under a second - Zero dependency changes: package.json is untouched relative to main markdown-preview-demo.md exercises every feature side by side with GitHub for review; happy to drop it before merge or keep it as a fixture. Links open in the OS browser, scheme-limited, with the destination shown on hover. Bare relative links render as plain text rather than dead anchors; dot-slash and fragment links keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The react-doctor toolchain entries had drifted 0.7.8 to 0.8.1 through an incidental pnpm install re-resolution; package.json never changed, so main's lockfile is exactly correct. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nk policy From a five-agent review pass. Failure paths become diagnosable: the watch-listener registration logs on rejection and the preview error boundary logs the error it contains. The link click policy is extracted to handleLinkClick and exercised directly (fragment scroll, OS-browser open, javascript: and file: never open). New locks pin the real boundaries: the assetProtocol scope the image resolver relies on, the Streamdown tuple shapes the pipeline destructures, javascript: href neutralization, the chat run-button default. Brittle source locks the testing guide exempts (theme CSS text, import order, formatting-exact plugin regex) are pruned or made format-insensitive. Net +7 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Element.scrollIntoView scrolls every scrollable ancestor, including the pane's overflow-hidden wrapper and the tab stack above it. Hidden overflow ignores the wheel, so once those ancestors were scrolled the user could never scroll them back: the pane stayed sheared with a blank bottom after clicking a TOC link near the end of the document. scrollToFragment computes the offset against the pane's own overflow-auto container and scrolls only that. Test fakes now omit scrollIntoView entirely, so a regression back to it throws. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bda7495 to
a139230
Compare
Footnote back-references target the raised sup anchor (line-height 0, -0.5em relative shift in the GitHub stylesheet); aligning that rect to the exact container top clipped the line containing the reference, so the view appeared to land below it. Inline targets now scroll their enclosing block and every landing keeps a small cushion above it, clamped at the document top. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…zoom Ctrl+= zoom is CSS zoom (zoom-content / --app-zoom), not native webview zoom. Chromium splits geometry under CSS zoom: getBoundingClientRect reports visual px while scrollTop and offsetHeight stay layout px, so feeding the visual rect delta into scrollTo overshot proportionally to zoom and distance. The delta is now divided by the scroller's measured scale (visual height over offsetHeight, exactly the zoom factor, 1 when unzoomed). offsetHeight rather than clientHeight so a wide table's horizontal scrollbar cannot poison the ratio. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trims multi-paragraph docblocks to short whys per TERAX.md, removes the stray ponytail comment tags, and swaps a literal emoji test fixture for its unicode escape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit pass over the 132 new tests: near-identical fixtures collapse into case tables inside one it() per contract, agreed-redundant tests are deleted, and the source-text config greps become object-level asserts on the exported pipeline array and sanitizer schema. Every security deny path, race guard, and dependency pin keeps its assertions; the module drops from 132 to 59 reported tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontmatter table transposed GitHub's layout, putting keys in a header row with all values in one row beneath. Each entry now gets its own row with the key in a row-scoped header cell; the test locks the orientation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The article carried mx-auto, but the vendored stylesheet's unlayered margin: 0 on .markdown-body outranks layered Tailwind utilities, so the column pinned left. The 980px centered column now lives in markdown-theme.css, which loads after the base file and wins by source order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/modules/markdown/MarkdownPreviewPane.tsx (1)
87-90: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winHidden preview panes keep watching and re-parsing on every save. The effect is keyed on
[path]andvisiblenever gatessyncPreviewFile, so a hidden-but-mounted preview tab still re-reads the file and pushes a newreadystatus (a fullRenderedMarkdownstatic parse, up to your measured near-second on large docs) on every external write. Unlike a PTY, nothing is lost by deferring: you can re-sync when the tab becomes visible. For a terminal-first, lightweight app this is avoidable idle CPU. Consider skipping the re-read while hidden and refreshing on the hidden to visible transition.🤖 Prompt for 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. In `@src/modules/markdown/MarkdownPreviewPane.tsx` around lines 87 - 90, Update the useEffect in MarkdownPreviewPane so syncPreviewFile does not run while visible is false, and trigger a fresh sync when the pane transitions back to visible. Preserve path changes while visible and the existing status update behavior.Source: Path instructions
🤖 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 `@src/modules/markdown/RenderedMarkdown.tsx`:
- Around line 159-195: Update PreviewErrorBoundary and its usage in
RenderedMarkdown so the boundary resets its failed state whenever the rendered
content changes. Pass content into the boundary and compare the previous and
current content during updates, clearing failed before retrying the new content
while preserving the existing error fallback and logging behavior.
---
Nitpick comments:
In `@src/modules/markdown/MarkdownPreviewPane.tsx`:
- Around line 87-90: Update the useEffect in MarkdownPreviewPane so
syncPreviewFile does not run while visible is false, and trigger a fresh sync
when the pane transitions back to visible. Preserve path changes while visible
and the existing status update behavior.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 1edd5e37-f9cc-43c2-b2c8-bca35b1ec717
📒 Files selected for processing (20)
markdown-preview-demo.mdsrc/components/ai-elements/chat-code.test.tsxsrc/components/ai-elements/chat-code.tsxsrc/components/ai-elements/markdown-code.tsxsrc/modules/markdown/MarkdownPreviewPane.test.tssrc/modules/markdown/MarkdownPreviewPane.tsxsrc/modules/markdown/RenderedMarkdown.test.tsxsrc/modules/markdown/RenderedMarkdown.tsxsrc/modules/markdown/frontmatter.test.tssrc/modules/markdown/frontmatter.tssrc/modules/markdown/githubAlerts.test.tsxsrc/modules/markdown/githubAlerts.tssrc/modules/markdown/headingAnchors.test.tsxsrc/modules/markdown/headingAnchors.tssrc/modules/markdown/localImages.test.tsxsrc/modules/markdown/localImages.tssrc/modules/markdown/markdown-base.csssrc/modules/markdown/markdown-theme.csssrc/modules/markdown/tableDirectives.test.tsxsrc/modules/markdown/tableDirectives.ts
The boundary flipped to failed and never flipped back. Panes stay mounted across a watcher refresh, so one render failure -- a deep-nesting stack overflow, say -- pinned the fallback for the life of the tab: fixing the file on disk could not bring the preview back, only toggling to Raw and back, which remounts the pane. getDerivedStateFromProps now clears the flag when the content differs from whatever failed, so the next render retries it; identical content keeps the fallback instead of looping on a render that is still broken. Chosen over componentDidUpdate with setState so recovery costs one render pass rather than committing the fallback and swapping it out, and so the reset stays testable as a pure function -- this repo has no DOM harness to drive a mount-then-update cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
GitHub-fidelity markdown preview: markdown files render the way GitHub renders them, built entirely on the app's existing Streamdown renderer with a rebuilt rehype pipeline.
At a glance: GitHub-fidelity styling on Terax theme tokens, frontmatter tables, table layout directives, GFM alerts, heading anchors with working TOC links, relative image resolution, live refresh on external file changes, hardened sanitization, static-only rendering, and zero dependency changes.
Why
Closes #434 (markdown "properly viewed as it is on GitHub"); also covers what #518 asked for, without the extension baggage. Everything rides Streamdown's own pipeline, keeping one markdown renderer in the app.
How
Look like GitHub
<!-- table width="100%" cols="12%, auto, 25%" -->) controls width and column sizing. Invisible on GitHub, which renders the plain tableBehave like GitHub
user-content-clobber GitHub applies). README TOC links scroll within the pane instead of navigating the webviewUnderstand the workspace
convertFileSrc, the same asset-protocol path the editor and media viewer use (feat(UI): Implement local media and PDF viewers #314 pattern)git checkout), wired to the feat(fs): live filesystem watcher for explorer tree and open editors #488 watcher, keeping scroll position; no flash through the loading statePipeline design
mode="static"withparseIncompleteMarkdown={false}, preserving the fix(markdown): render file previews statically #913 invariant. A 7k-line document is a single parse in roughly 0.6 s, guard-testedrehypePluginsto Streamdown replaces its internal chain wholesale, so the pipeline is rebuilt from Streamdown's own exported defaults with the feature plugins spliced in: raw, table directives, alerts, heading anchors, local images, then sanitize, then harden. Sanitize runs after every plugin; nothing bypasses itcolgroup/coltags, the exactmarkdown-alertclass values, and theassetscheme forimg src. A test locks the schema against blanketclassNameallowancesWhere the lines go (the diff looks heavy for a preview feature; +3,704 lines total)
Net: the hand-written program logic is roughly 660 lines; the rest is stylesheet, tests, documentation and the demo.
Testing
549 tests green; 59 across the markdown module and shared code components cover the directives, alerts, anchors, image resolution, watch sync, frontmatter, link policy and error boundary. Injection fixtures verify script tags, iframes, style tags, inline event handlers and
javascript:hrefs (including mixed-case) render inert, while GitHub-allowed raw HTML (kbd,details,sub/sup) still renders. The load-bearing external assumptions are pinned by tests: theassetProtocolscope the image resolver relies on, and the Streamdown internal tuple shapes the pipeline destructures (a minor bump that reshapes them fails loudly). A pathological-nesting fixture (about 2k nested blockquotes) fails soft inside the pane's error boundary, which logs the contained error. A five-agent review pass (code quality, test coverage, comment accuracy, silent failures, testing-guide adherence) preceded this PR; its findings are folded into the final commits.Manual flows exercised in
pnpm tauri devagainst markdown-preview-demo.md: all five alert types plus reject cases in light and dark themes, TOC fragment links (including bottom-of-document targets, which surfaced and fixed a scroll-container bug), relative and missing images, Raw/Rendered toggle, and the GitHub side-by-side comparison. Live refresh on external edits is verified end to end: unit and race tests, plus a manual pass with an external process editing the previewed file (pane updated in place, scroll held, no loading flash). Note for reviewers trying this in dev: use a file outside the project root, because Vite full-reloads the dev webview for in-root changes and masks the pane-level refresh.pnpm lintclean (no new warnings; the 103 pre-existing on main are unchanged)pnpm check-typescleanpnpm testcleansrc-tauri/)cargo clippyclean: N/A,src-tauri/untouchedsrc-tauri/)cargo nextest runclean: N/A,src-tauri/untouched#[tauri::command]signature) called out below: N/A, no command signatures changedpnpm tauri devScreenshots
Referencing the same file from current build (old) vs. PR branch (new) - I chose dark theme as a wild guess that it's more common than light. It makes some of the elements harder to see when the images are smaller sizes - worth clicking to open for a better visual comparison. As noted below, the test file markdown-preview-demo.md is included in the root directory to help with review (can/should be removed before merge).
Old:


New (front matter table, list indentation, "inline code" formatting, footnote reference formatting, heading underlines, line spacing, consistent page width):
Old:


New (alert formatting, list indentation, list spacing, nested list indentation, nested list numbering, task list formatting):
Old:


New (table formatting, table contents formatting, custom column width features):
Old:


New (removed broken > run button for shell commands, fixed plain fence formatting, added relative path image resolving):
Old:


New (consistent inline html, slug duplicate fix, horizontal rule formatting, footnote formatting):
Notes for reviewer
docs/x.md) render as plain text instead of the dead anchors the preview produced before;./x.mdand#fragmentlinks keep working. Cross-file navigation is a possible follow-up/assets/x.png) pass through unresolved for now; GitHub resolves them against the repo root, and resolving them cleanly wants a workspace-root concept this module does not havemarkdown-preview-demo.mdat root in this PR - it exercises every feature with GitHub's rendering. I can drop it before merge (or move to docs).Summary by CodeRabbit
https/mailto, ignored/neutralized for unsafe links).