A Chrome extension that renders raw Markdown as readable HTML, in place.
Open a .md file in Chrome and you get unstyled plain text. This extension detects those pages and
renders them properly — headings, tables, syntax-highlighted code, diagrams, math — while keeping the
original URL and a one-click toggle back to the source.
Works on local files (file://) and on raw Markdown served over the web.
- GitHub Flavored Markdown — tables, task lists, strikethrough, autolinks
- Syntax highlighting for fenced code blocks via highlight.js
- Table of contents sidebar with scroll-spy, auto-generated from headings
- Mermaid diagrams —
```mermaidfences render as flowcharts, sequence diagrams, and more - Math —
$inline$and$$block$$LaTeX via KaTeX - Light and dark themes that follow your system setting
- Toggle to source at any time, with no refetch — works offline
- Nothing leaves your machine. No network requests, no analytics, no remote code.
Mermaid (~1 MB) and KaTeX (~250 kB) load only when a document actually contains them. A typical document loads neither.
Not yet on the Chrome Web Store, so installation is manual.
- Download the latest ZIP from Releases and unzip it
- Open
chrome://extensions - Enable Developer mode (top right)
- Click Load unpacked and select the unzipped folder
- Click Details on the extension card and enable Allow access to file URLs
No Node.js or build step required.
git clone https://github.com/cemhurturk/chrome-markdown-viewer.git
cd chrome-markdown-viewer
npm install
npm run buildThen follow steps 2–5 above, selecting the dist/ folder.
Step 5 is required for local files. Chrome does not allow extensions to request that permission
programmatically — you have to grant it by hand. Without it the extension still works on Markdown
served over the web, but local .md files keep showing as plain text. The extension opens a page
explaining this on first install.
Developer mode is unavoidable outside the Web Store. Chrome has blocked installing extensions from outside the store since Chrome 33, so there is no one-click install from GitHub. Chrome will show a "Disable developer mode extensions" prompt on startup, and some managed work profiles block Developer mode entirely.
Content-Type: text/markdown triggers a download instead of a render. When a web server sends
that header, Chrome downloads the file rather than displaying it, so no content script ever runs and
the extension cannot help. Servers sending text/plain — including GitHub raw URLs — work fine.
Strict page CSPs can block diagrams and math. Mermaid and KaTeX load at runtime as separate
chunks. A page whose Content-Security-Policy forbids that (raw.githubusercontent.com sends
default-src 'none') will still render Markdown, but diagrams fall back to an error card and math
stays as raw LaTeX text.
Non-ASCII headings get generic anchor ids. A heading in CJK or Cyrillic produces section,
section-1, and so on. The TOC links still work; the anchors are just not meaningful.
Toggling to the source view on a local file:// page gives you an editable textarea instead of
read-only text. This only works on file:// pages — there is no writeback path for http(s)
URLs, so Markdown served over the web stays read-only source.
The first time you save, Chrome shows a native file-save dialog so you confirm which file on disk
you're writing to. Every later save in that same tab writes to the same file silently, with no
further dialog. Cmd+S (or Ctrl+S on Windows/Linux) saves from the textarea, same as the Save
button. If you close or navigate away from the tab with unsaved edits, Chrome warns you before
letting you leave.
Chrome already displays .md files as plain text inside a <pre> element. A content script reads
that text, renders it, and replaces the document body — so the URL never changes, bookmarks keep
working, and reload behaves normally.
| Module | Responsibility |
|---|---|
src/detect.ts |
Pure predicate: is this page raw Markdown? |
src/render.ts |
Pure transform: Markdown → sanitized HTML + heading metadata |
src/content.ts |
The only impure unit — reads the DOM, swaps the page, wires the toggle |
src/toc.ts |
Sidebar construction and scroll-spy |
src/lazy.ts |
Runtime loading of Mermaid/KaTeX, with per-block error handling |
src/background.ts |
Opens the onboarding page once on install |
detect.ts and render.ts are pure functions — string in, value out — which is why the bulk of the
test suite needs no browser.
The extension renders a page only when all of these hold:
- The document body contains exactly one element child, a
<pre>(Chrome's plain-text signature) - The URL path ends in
.md,.markdown, or.mdown— a.mdin a query string does not count - The page has not already been rendered
A false negative just means you see raw text. A false positive would mean destroying a real webpage's DOM. The second is far worse, so the rules err toward doing nothing.
A .md file is fully untrusted input — it can contain <script> tags, onerror= handlers, and
javascript: URLs.
- All rendered HTML passes through DOMPurify before entering the DOM
- Author-supplied
idattributes are stripped, so a document cannot hijack a TOC anchor <style>is forbidden in rendered Markdown — including inside<svg>, where it would otherwise apply document-wide and could blank the page- Mermaid runs with
securityLevel: 'strict'; KaTeX runs withtrust: false - No remote code is loaded; every dependency is bundled, as Manifest V3 requires
The XSS test cases in the suite are treated as non-negotiable.
A rendering failure always degrades to readable text, never to a broken page.
| Failure | Behavior |
|---|---|
| Markdown parsing throws | Page left completely untouched — you see the raw source |
| A Mermaid diagram is malformed | That block shows an inline error card; the rest of the page renders |
| A math expression is malformed | Same — a per-block error card, document survives |
| Mermaid or KaTeX fails to load | Affected blocks fall back to displaying their source text |
npm test # run the test suite (93 tests)
npm run build # build to dist/After rebuilding, click the reload icon on the extension card at chrome://extensions.
The build produces three bundles for a reason. Manifest V3 content scripts declared in the manifest
cannot be ES modules, so content.js is a self-contained IIFE. But await import() at runtime
requires an ES module target, so the Mermaid and KaTeX chunks are built separately as ESM and exposed
through web_accessible_resources. The service worker is a third IIFE bundle.
Contributions are welcome. If you change rendering behavior, please add a test — and if you touch sanitization, do not weaken an existing XSS test to make something pass.
marked · DOMPurify · highlight.js · Mermaid · KaTeX
KaTeX's stylesheet and WOFF2 fonts are vendored into public/ because Manifest V3 forbids loading
them remotely. They remain under KaTeX's MIT license.
MIT — see LICENSE.