Claude
Skills
Sign in
Back

md-preview-app-macos

Included with Lifetime
$97 forever

Native macOS Markdown viewer app with Quick Look extension, Mermaid diagrams, KaTeX math, document outline, and editor integration

Writing & Docs

What this skill does


# 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 [email protected]: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 cl

Related in Writing & Docs