Skip to content

feat(markdown): GitHub-style rendering for markdown preview - #1037

Open
Gimics wants to merge 11 commits into
crynta:mainfrom
Gimics:feat/markdown-github-preview
Open

feat(markdown): GitHub-style rendering for markdown preview#1037
Gimics wants to merge 11 commits into
crynta:mainfrom
Gimics:feat/markdown-github-preview

Conversation

@Gimics

@Gimics Gimics commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

  • Vendored github-markdown-css (MIT, sindresorhus) mapped onto the app's theme tokens, so custom themes keep working in both light and dark
  • Frontmatter renders as a table, like GitHub
  • Table layout directives: an HTML comment above a table (<!-- table width="100%" cols="12%, auto, 25%" -->) controls width and column sizing. Invisible on GitHub, which renders the plain table

Behave like GitHub

  • GFM alerts, all five types, matching GitHub's semantics (case-insensitive marker on its own first line, unknown types stay blockquotes, no nesting). Icons are CSS mask data URIs, so the sanitizer stays closed to svg
  • Heading anchors with GitHub-slug ids (including the user-content- clobber GitHub applies). README TOC links scroll within the pane instead of navigating the webview

Understand the workspace

Pipeline design

  • One markdown renderer in the app: Streamdown, mode="static" with parseIncompleteMarkdown={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-tested
  • Passing rehypePlugins to 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 it
  • Sanitizer additions are enumerated only: colgroup/col tags, the exact markdown-alert class values, and the asset scheme for img src. A test locks the schema against blanket className allowances
  • Streamdown's link-safety popup is disabled in the preview only (chat untouched); links open in the OS browser, scheme-limited to http(s)/mailto, with the true destination shown on hover

Where the lines go (the diff looks heavy for a preview feature; +3,704 lines total)

Category Lines Share What it is
Vendored stylesheet ~1,400 39% github-markdown-css checked in as an auditable source rather than added as a dependency
Tests ~1,300 34% Tests outweigh production code 1.7:1: injection fixtures, watcher race coverage, path-traversal edges, sanitizer schema and asset-scope locks
Production code ~740 20% Six small modules; comments are held to short whys on security invariants and pipeline ordering.
Review demo ~230 6% markdown-preview-demo.md, droppable before merge
Lockfile / deps 0 0 package.json and pnpm-lock.yaml are byte-identical to main

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: the assetProtocol scope 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 dev against 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 lint clean (no new warnings; the 103 pre-existing on main are unchanged)
  • pnpm check-types clean
  • pnpm test clean
  • Manual smoke-test of the affected feature
  • (If you touched src-tauri/) cargo clippy clean: N/A, src-tauri/ untouched
  • (If you touched src-tauri/) cargo nextest run clean: N/A, src-tauri/ untouched
  • (If you changed a #[tauri::command] signature) called out below: N/A, no command signatures changed
  • (If UI) tested in pnpm tauri dev
  • Platforms tested: Windows
  • Shells tested (if relevant): N/A, no shell interaction

Screenshots

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:
image
New (front matter table, list indentation, "inline code" formatting, footnote reference formatting, heading underlines, line spacing, consistent page width):
image

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

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

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

Old:
image
New (consistent inline html, slug duplicate fix, horizontal rule formatting, footnote formatting):
image

Notes for reviewer

  • One deliberate behavior change: bare relative links (docs/x.md) render as plain text instead of the dead anchors the preview produced before; ./x.md and #fragment links keep working. Cross-file navigation is a possible follow-up
  • Root-absolute image paths (/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 have
  • Size gates green: startup 342.84 kB of 540 kB, total client JS 1.48 MB of 1.5 MB; the markdown stack stays a lazy chunk. Math and mermaid are intentionally not here (math is Render LaTeX math in chat and markdown previews #714's territory)
  • I left a markdown-preview-demo.md at root in this PR - it exercises every feature with GitHub's rendering. I can drop it before merge (or move to docs).
  • Happy to split any of the feature groups into separate PRs if that makes reviews easier

Summary by CodeRabbit

  • New Features
    • Added a hardened Markdown preview supporting YAML frontmatter, GitHub-style alerts, task lists, tables (with width/columns directives), fenced code blocks, images, footnotes, HTML, and heading anchors.
    • Enabled local image URL rewriting, fragment navigation (in-preview scrolling), and safer link handling (external open for https/mailto, ignored/neutralized for unsafe links).
    • Added live file refresh with safer update behavior and a friendly fallback when rendering fails.
    • Made the “Run in active terminal” control optional for command blocks.
  • Documentation
    • Added a Markdown preview demo documentation file covering supported features.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Markdown preview pipeline

Layer / File(s) Summary
Static renderer and safety pipeline
src/modules/markdown/RenderedMarkdown.tsx, src/modules/markdown/RenderedMarkdown.test.tsx, src/components/ai-elements/chat-code.tsx
Adds static Streamdown rendering, sanitized URL policies, link interception, error fallback, inert frontmatter display, and disables terminal execution in previews.
Frontmatter extraction
src/modules/markdown/frontmatter.ts, src/modules/markdown/frontmatter.test.ts
Parses conservative YAML frontmatter into ordered entries while preserving unsupported or invalid input unchanged.
Markdown AST transformations
src/modules/markdown/{githubAlerts,headingAnchors,localImages,tableDirectives}.*
Adds alert conversion, heading IDs and fragment scrolling, relative image asset resolution, and table width/column directives with rendering coverage.
Preview file synchronization
src/modules/markdown/MarkdownPreviewPane.tsx, src/modules/markdown/MarkdownPreviewPane.test.ts
Extracts watched file reads into syncPreviewFile, handling path matching, cleanup, stale reads, refresh state, and read failures.
Preview presentation
src/modules/markdown/markdown-base.css, src/modules/markdown/markdown-theme.css, markdown-preview-demo.md
Adds GitHub Markdown styling, application theme overrides, alert icons, table/list/code styling, and a feature demonstration document.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • crynta/terax-ai#913: Also changes Markdown preview rendering to Streamdown static mode and adjusts related preview wiring.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits and clearly summarizes the markdown preview rendering changes.

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.

@Gimics

Gimics commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@Gimics
Gimics force-pushed the feat/markdown-github-preview branch 5 times, most recently from b20c8f4 to bda7495 Compare July 22, 2026 22:53
Gimics and others added 4 commits July 23, 2026 18:12
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>
@Gimics
Gimics force-pushed the feat/markdown-github-preview branch from bda7495 to a139230 Compare July 24, 2026 03:30
Gimics and others added 6 commits July 23, 2026 20:59
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>
@Gimics
Gimics marked this pull request as ready for review July 24, 2026 07:54
@Gimics
Gimics requested a review from crynta as a code owner July 24, 2026 07:54

@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: 1

🧹 Nitpick comments (1)
src/modules/markdown/MarkdownPreviewPane.tsx (1)

87-90: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hidden preview panes keep watching and re-parsing on every save. The effect is keyed on [path] and visible never gates syncPreviewFile, so a hidden-but-mounted preview tab still re-reads the file and pushes a new ready status (a full RenderedMarkdown static 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

📥 Commits

Reviewing files that changed from the base of the PR and between 40c4c89 and 9963a62.

📒 Files selected for processing (20)
  • markdown-preview-demo.md
  • src/components/ai-elements/chat-code.test.tsx
  • src/components/ai-elements/chat-code.tsx
  • src/components/ai-elements/markdown-code.tsx
  • src/modules/markdown/MarkdownPreviewPane.test.ts
  • src/modules/markdown/MarkdownPreviewPane.tsx
  • src/modules/markdown/RenderedMarkdown.test.tsx
  • src/modules/markdown/RenderedMarkdown.tsx
  • src/modules/markdown/frontmatter.test.ts
  • src/modules/markdown/frontmatter.ts
  • src/modules/markdown/githubAlerts.test.tsx
  • src/modules/markdown/githubAlerts.ts
  • src/modules/markdown/headingAnchors.test.tsx
  • src/modules/markdown/headingAnchors.ts
  • src/modules/markdown/localImages.test.tsx
  • src/modules/markdown/localImages.ts
  • src/modules/markdown/markdown-base.css
  • src/modules/markdown/markdown-theme.css
  • src/modules/markdown/tableDirectives.test.tsx
  • src/modules/markdown/tableDirectives.ts

Comment thread src/modules/markdown/RenderedMarkdown.tsx
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown Viewer

1 participant