md-preview-app-macos
Native macOS Markdown viewer app with Quick Look extension, Mermaid diagrams, KaTeX math, document outline, and editor integration
Best use case
md-preview-app-macos is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Native macOS Markdown viewer app with Quick Look extension, Mermaid diagrams, KaTeX math, document outline, and editor integration
Teams using md-preview-app-macos should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/md-preview-app-macos/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How md-preview-app-macos Compares
| Feature / Agent | md-preview-app-macos | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Native macOS Markdown viewer app with Quick Look extension, Mermaid diagrams, KaTeX math, document outline, and editor integration
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# MD Preview App — macOS Markdown Viewer
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
A fast, native macOS app (AppKit + WKWebView) for reading `.md` files. No Electron, no browser tab. Features include a document outline sidebar, Mermaid diagram rendering, KaTeX math, Quick Look extension, in-document search, and one-click "Open With" for popular editors.
---
## Installation
### Pre-built (recommended)
Download the signed and notarized DMG from the [Releases page](https://github.com/pluk-inc/md-preview.app/releases) and drag to `/Applications`.
### Build from source
```sh
git clone git@github.com:pluk-inc/md-preview.app.git
cd md-preview.app
open md-preview.xcodeproj
# Build and run the `md-preview` scheme
# SPM resolves Sparkle + swift-markdown on first build
```
**Requirements:** macOS 15+, Xcode with Swift 6.0, Apple Silicon or Intel.
---
## Project Layout
```
md-preview/ # Main app target (AppKit, WKWebView)
quick-look/ # Quick Look extension (.appex)
scripts/ # Release & rollback automation
Version.xcconfig # MARKETING_VERSION + CURRENT_PROJECT_VERSION
appcast.xml # Sparkle update feed
```
---
## Key Dependencies (Swift Package Manager)
| Package | Purpose |
|---|---|
| `swift-markdown` (Apple) | Markdown parsing via cmark-gfm |
| `Sparkle` | Auto-update framework |
| Mermaid (bundled JS) | Fenced `mermaid` block rendering |
| KaTeX (bundled JS) | LaTeX math rendering |
---
## Core Architecture
### Markdown → HTML Pipeline
```swift
import Markdown
// Parse a markdown string into a Document
let source = try String(contentsOf: fileURL, encoding: .utf8)
let document = Document(parsing: source)
// Walk the document tree to build HTML + extract headings for TOC
struct HTMLRenderer: MarkupVisitor {
typealias Result = String
mutating func visitDocument(_ document: Document) -> String {
document.children.map { visit($0) }.joined()
}
mutating func visitHeading(_ heading: Heading) -> String {
let text = heading.plainText
let anchor = text.lowercased()
.replacingOccurrences(of: " ", with: "-")
.filter { $0.isLetter || $0.isNumber || $0 == "-" }
let level = heading.level
return "<h\(level) id=\"\(anchor)\">\(text)</h\(level)>\n"
}
mutating func visitParagraph(_ paragraph: Paragraph) -> String {
"<p>\(paragraph.children.map { visit($0) }.joined())</p>\n"
}
mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> String {
let lang = codeBlock.language ?? ""
if lang == "mermaid" {
// Render as Mermaid diagram via bundled mermaid.min.js
return "<div class=\"mermaid\">\(codeBlock.code)</div>\n"
}
if lang == "math" {
// Render as KaTeX display math
return "<div class=\"math-display\">$$\(codeBlock.code)$$</div>\n"
}
return "<pre><code class=\"language-\(lang)\">\(codeBlock.code)</code></pre>\n"
}
}
```
### Loading HTML into WKWebView
```swift
import WebKit
class PreviewViewController: NSViewController, WKNavigationDelegate {
let webView = WKWebView()
func loadMarkdown(from url: URL) {
let source = try! String(contentsOf: url, encoding: .utf8)
var renderer = HTMLRenderer()
let body = renderer.visit(Document(parsing: source))
let html = wrapInTemplate(body)
// Load with base URL so bundled assets (mermaid, KaTeX) resolve
let resourceURL = Bundle.main.resourceURL!
webView.loadHTMLString(html, baseURL: resourceURL)
}
// Handle navigation: open external links in default browser
func webView(_ webView: WKWebView,
decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if action.navigationType == .linkActivated,
let url = action.request.url,
url.scheme == "https" || url.scheme == "http" {
NSWorkspace.shared.open(url)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
```
### Document Outline (TOC) Sidebar
```swift
struct HeadingItem: Identifiable {
let id = UUID()
let level: Int
let text: String
let anchor: String
}
// Extract headings while parsing
func extractHeadings(from document: Document) -> [HeadingItem] {
var headings: [HeadingItem] = []
for child in document.children {
if let heading = child as? Heading {
let text = heading.plainText
let anchor = text.lowercased()
.replacingOccurrences(of: " ", with: "-")
headings.append(HeadingItem(level: heading.level,
text: text,
anchor: anchor))
}
}
return headings
}
// Jump to heading via JavaScript
func scrollTo(anchor: String) {
let js = "document.getElementById('\(anchor)')?.scrollIntoView({behavior:'smooth'})"
webView.evaluateJavaScript(js, completionHandler: nil)
}
```
---
## Quick Look Extension
The extension lives in `quick-look/` and is a separate `.appex` target. It reuses the same HTML rendering pipeline so Mermaid diagrams and math work offline in Finder spacebar previews.
```swift
// quick-look/PreviewViewController.swift (skeleton)
import Quartz
class PreviewViewController: NSViewController, QLPreviewingController {
func preparePreviewOfFile(at url: URL,
completionHandler: @escaping (Error?) -> Void) {
let source = try! String(contentsOf: url, encoding: .utf8)
var renderer = HTMLRenderer()
let html = wrapInTemplate(renderer.visit(Document(parsing: source)))
let base = Bundle.main.resourceURL!
webView.loadHTMLString(html, baseURL: base)
completionHandler(nil)
}
}
```
---
## Mermaid Diagrams
Fenced `mermaid` blocks are detected during parsing and emitted as `<div class="mermaid">` elements. The bundled `mermaid.min.js` initializes on page load — no CDN required.
**Markdown input:**
````markdown
```mermaid
flowchart TD
A[Input .md] --> B[swift-markdown]
B --> C[WKWebView]
```
````
**HTML template snippet (how it's wired):**
```html
<script src="mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
```
---
## KaTeX Math
| Syntax | Usage |
|---|---|
| `$x^2 + y^2$` | Inline math |
| `$$\int_0^1 f(x)\,dx$$` | Display math |
| ` ```math ` block | Fenced display math |
Copying a rendered formula pastes the original LaTeX source (via the bundled `copy-tex` KaTeX extension).
---
## "Open With" Editor Integration
The app queries Launch Services for apps that declare an editor role for Markdown UTIs, filters to known editors, and remembers your pick.
```swift
let mdUTI = UTType("net.daringfireball.markdown")!
let editors = NSWorkspace.shared.urlsForApplications(
toOpen: fileURL // or query by UTI
).filter { url in
let knownEditors = ["com.microsoft.VSCode",
"com.todesktop.230313mzl4w4u92", // Cursor
"dev.zed.zed", "com.sublimetext.4",
"com.barebones.bbedit", "com.panic.Nova",
"com.coteditor.CotEditor", "com.macromates.TextMate",
"org.vim.MacVim", "com.apple.dt.Xcode",
"com.apple.TextEdit"]
let bundleID = Bundle(url: url)?.bundleIdentifier ?? ""
return knownEditors.contains(bundleID)
}
// Open the file in chosen editor
NSWorkspace.shared.open([fileURL],
withApplicationAt: editorURL,
configuration: .init(),
completionHandler: nil)
```
---
## Share / Copy Source
The Share toolbar item feeds the Markdown *text* (not a file URL) to `NSSharingServicePicker`, so **Copy** writes raw Markdown to the clipboard — ideal for pasting into ChatGPT or Claude.
```swift
// Wire up the share button
@objc func shareDocument(_ sender: NSToolbarItem) {
let source = try! String(contentsOf: currentFileURL, encoding: .utf8)
let picker = NSSharingServicePicker(items: [source])
picker.show(relativeTo: .zero, of: sender.view!, preferredEdge: .minY)
}
```
---
## In-Document Search
Standard `WKWebView` find interaction — no custom implementation needed:
```swift
// Enable find bar (macOS 12+)
webView.configuration.preferences.setValue(true,
forKey: "developerExtrasEnabled")
// Trigger search (connect to ⌘F)
@objc func performFindPanelAction(_ sender: Any?) {
webView.performFindPanelAction(sender)
}
// ⌘G / ⌘⇧G handled automatically by WKWebView
```
---
## Supported File Types
| Extension | UTI |
|---|---|
| `.md`, `.markdown`, `.mdown` | `net.daringfireball.markdown` |
| `.txt` | `public.plain-text` |
Register in `Info.plist` under `CFBundleDocumentTypes` and `UTImportedTypeDeclarations`.
---
## Releasing
Releases use [Amore](http://amore.computer/) for signing, notarization, DMG, S3 upload, and Sparkle appcast.
```sh
# 1. Bump versions (single source of truth)
# Edit Version.xcconfig:
# MARKETING_VERSION = 1.2.0
# CURRENT_PROJECT_VERSION = 42
# 2. Run release script
./scripts/release.sh
# 3. Roll back a bad release
./scripts/rollback-release.sh
```
---
## Contributing Workflow
```sh
# Fork, then:
git clone https://github.com/YOUR_FORK/md-preview.app.git
cd md-preview.app
# Create a feature branch
git checkout -b feature/my-change
# Open in Xcode, build the `md-preview` scheme
open md-preview.xcodeproj
# Manual smoke test before PR:
# - Drop a .md with headings, mermaid blocks, and math onto the app
# - Verify TOC sidebar, diagram render, math render
# - Test Quick Look (spacebar in Finder)
# - Test "Open With" menu
git push origin feature/my-change
# Open PR against main
```
**PR guidelines:**
- One logical change per PR
- Open an issue first for larger changes
- Match existing Swift style (no formatter enforced — mirror nearby code)
- No UI test suite yet; manual smoke test required for UI changes
---
## Troubleshooting
| Problem | Fix |
|---|---|
| Mermaid diagrams blank | Check `baseURL` points to app bundle resources; `mermaid.min.js` must be in the Copy Bundle Resources phase |
| KaTeX not rendering | Same — verify `katex.min.js`, `katex.min.css`, and `copy-tex.min.js` are in bundle resources |
| Quick Look shows plain text | Re-run `qlmanage -r` to reset Quick Look daemon: `qlmanage -r && qlmanage -r cache` |
| SPM not resolving | `File → Packages → Reset Package Caches` in Xcode |
| Notarization issues | Use Amore or check `xcrun notarytool` — requires `APPLE_ID`, `TEAM_ID`, and an app-specific password |
| App not set as default handler | Delete `~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2` and re-register via `LSSetDefaultHandlerForURLScheme` or the app's first-launch prompt |
```sh
# Reset Quick Look plugin cache after building
qlmanage -r
qlmanage -r cache
# Test Quick Look directly
qlmanage -p /path/to/file.md
```Related Skills
whatcable-macos-usb-inspector
macOS menu bar app that identifies USB-C cable capabilities and charging diagnostics using IOKit
type4me-macos-voice-input
MacOS voice input tool with local/cloud ASR engines, LLM text optimization, and fully local storage built in Swift
puremac-macos-cleaner
Free open-source macOS cleaner built with SwiftUI — CleanMyMac alternative with zero telemetry, scheduled auto-cleaning, and Xcode/Homebrew/system cache cleanup.
capso-screenshot-macos
Expert skill for Capso, the open-source macOS screenshot and screen recording app built with Swift 6 and SwiftUI — covers architecture, building from source, package APIs, and contributing.
```markdown
---
zeroboot-vm-sandbox
Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot
yourvpndead-vpn-detection
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
xata-postgres-platform
Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment
x-mentor-skill-nuwa
AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.
wx-favorites-report
End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.
wterm-web-terminal
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings
worldmonitor-intelligence-dashboard
Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking