Skip to content
leok7vPublic

About

Less is more. Holds for Markdown too.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Repository files navigation

md.too

A minimalist Markdown viewer for macOS and iOS. Read-only, native, zero third-party dependencies.

What it does

  • Open a .md file and see it rendered.
  • Read selectably — drag across paragraphs, headings, lists, and quotes and the selection flows continuously the way the document reads. Code blocks, tables, and images select as whole atoms.
  • Copy the whole document (Cmd-C or the toolbar Copy button) — paste it as plain text, HTML, RTF, or PDF depending on what the target app accepts.
  • Save the document as a paginated PDF (with syntax-highlighted code, embedded images, zebra-striped tables) or as a self-contained HTML file (inline styles only — no external CSS, no JavaScript).
  • Toggle between the rendered view and the raw Markdown source from the toolbar.
  • macOS Quick Look extension renders .md in Finder's preview pane and on spacebar peek.
  • Syntax highlighting for about 40 languages (Swift, C/C++/C#/Objective-C, Java, Kotlin, Scala, JS/TS, Python, Rust, Go, Ruby, PHP, Dart, Lua, Perl, R, Julia, Haskell, OCaml, F#, Elixir, Clojure, Groovy, SQL, GraphQL, Dockerfile, Makefile, TOML, INI, YAML, JSON, XML, HTML, CSS, Bash, PowerShell, Markdown).
  • Tables with zebra rows, a stronger header band, column alignment from the delimiter row (:---, :---:, ---:), escaped pipes, and inline formatting inside cells — **bold**, *italic*, `code`, ~~strike~~, [link](url).
  • GitHub-style task lists (- [ ] / - [x]).
  • The HTML fragments that models and README authors write when Markdown has no spelling: <br>, <small>, <sup>/<sub>, <u>, <kbd>, <a href>, <img src>, <b>/<i>/<s>, comments, <div align="center">, and <details> drawn open. Anything else stays as written.
  • Maths. A $$…$$ display is typeset properly -- fractions, radicals, big operators with limits, stretchy fences, matrices, labelled braces, boxes, struck and coloured terms -- by a TeX layout engine reading the OpenType MATH table. Inline $…$ is typeset too, on the baseline at the size of the text around it, in headings and table cells as well; the plain-text copy gives the TeX back.

See EXAMPLE.md for a single document that exercises every supported feature.

What it doesn't do

No editor, no live edit/preview split, no autosave. No HTML or WKWebView. No file tree, tabs, or command palette. No app-level themes (light/dark follows the system, or pick one explicitly). No third-party packages — pure Swift + AppKit/UIKit/SwiftUI.

For context: the most popular JavaScript Markdown library, marked, reports about 636 transitive dependencies and roughly 38,980 lines of code on its public dependency graph. The md.too app is a few Swift files with no dependencies. Every package you do not pull in is a supply-chain risk you do not inherit.

Toolbar

The toolbar (in both apps, and a reduced cluster in the top-right of the Quick Look pane) carries these actions:

䷀ ⎘ ⤓ᴘᴅꜰ ⤓ₕₜₘₗ ↥ ◐☼☽

  • ䷀ Source — toggle between the rendered document and the raw Markdown text. Read-only either way; the source view exists so you can copy verbatim Markdown when the destination needs source rather than a formatted paste.
  • ⎘ Copy — copy the whole document to the clipboard with multiple flavors attached:
    • Plain text — falls into Terminal, an <input> field, or a code editor as readable Markdown-flavored text. Tables are width-aligned ASCII so columns line up.
    • HTML — pastes into Mail, Gmail, Notes, or any rich-text editor with bold / italic / code / links / tables / zebra striping preserved. Inline styles only, no <style> block, so paste sanitizers leave the formatting alone.
    • RTF (macOS) — pastes into TextEdit and other Cocoa rich-text targets.
    • PDF — drops a fully laid-out PDF into Pages, Keynote, or Preview. Rendered on demand the first time the target asks for it, so Copy itself stays instant.
  • ⤓ᴘᴅꜰ Save as PDF (macOS) — paginated PDF with embedded images, syntax-highlighted code blocks, zebra-striped tables, page numbers, and the document title in the header. The page size follows your locale (Letter in the US, A4 elsewhere).
  • ⤓ₕₜₘₗ Save as HTML (macOS) — single self-contained .html file with all images inlined as base64 and styles attached inline on every element. Theme-neutral grays, no JavaScript, no external references — opens and prints anywhere, survives email, and round-trips through TextEdit's HTML-to-RTF converter.
  • ↥ Share — system Share sheet with a freshly-rendered PDF attached.
  • ◐☼☽ Theme — system / light / dark cycle for the current window only. The document is never modified.

The Quick Look extension shows only the Source and Theme buttons; saving, copying, and sharing happen in the full app.

Why

A friend asked me last week what a Markdown file is. I had to explain that .md is the substrate the work is written on — README, AGENTS, PRD, every issue, every PR — and that I'd just spent a week cycling through half a dozen Electron viewers, each half a gigabyte of TypeScript shipping its own "Pro" upsell modal, and each still failing at a nested list inside a blockquote inside a code fence. So I lifted the Markdown renderer from an earlier chat-app project of mine and made it a real app. The macOS build also exports to a paginated PDF on one click, and the Quick Look extension renders any .md in Finder's preview pane and on spacebar peek. The iOS build exists because once the parser and view were portable, it was three Info.plist keys away.

Download

  • iOS / iPadOS — on the App Store.

  • macOS — signed and notarized .dmg published as a GitHub Release on each tagged version.

  • macOS with Homebrew:

    brew tap leok7v/tap
    brew trust leok7v/tap
    brew install --cask md-too
    

Don't do both! The Mac App Store copy and the Homebrew copy are the same app. Install one of them, not both.

Build from source

md.too.xcodeproj is NOT committed: it is generated from project.yml (the source of truth) by XcodeGen. So build settings live in project.yml, not the Xcode UI, and there are no project.pbxproj merge conflicts. Install XcodeGen once, then regenerate the project after cloning and after any project.yml change:

brew install xcodegen
xcodegen generate

Scripts/xcgen.sh does the same from any directory. Then open md.too.xcodeproj in Xcode 16 or later and pick a scheme:

  • md.too — the multiplatform app. Pick a macOS or iOS destination in the toolbar; on macOS the Quick Look extension is embedded automatically.
  • md.too QuickLook — the Quick Look extension on its own; normally not needed.

Or from the command line:

xcodebuild -project md.too.xcodeproj -scheme "md.too" \
  -destination "generic/platform=macOS" build
xcodebuild -project md.too.xcodeproj -scheme "md.too" \
  -destination "generic/platform=iOS Simulator" build

The project ships with no DEVELOPMENT_TEAM set, so a fresh clone builds with ad-hoc ("Sign to Run Locally") signing — no Apple Developer account required for local development. To override locally, drop a one-line Local.xcconfig next to Base.xcconfig containing LOCAL_DEVELOPMENT_TEAM = YOURTEAMID; it's gitignored.

The tests live in tests/ as the md.too Tests target: a golden dump of the parser over every section of EXAMPLE.md, a golden of the HTML export, the KaTeX layout engine fingerprinted by metrics and pixels, the incremental splice, the maths cache, and the plain-text export as a round-trip. Run them with:

xcodebuild test -project md.too.xcodeproj -scheme "md.too" \
  -destination "platform=macOS"

A golden that moved on purpose is re-recorded by setting TEST_RUNNER_PARSER_GOLDEN_UPDATE=1, TEST_RUNNER_HTML_GOLDEN_UPDATE=1 or TEST_RUNNER_KATEX_GOLDEN_UPDATE=1 on that command, then reading the diff before committing it.

Privacy

Privacy policy — short version: the app collects nothing, stores nothing, transmits nothing. The only network requests are HTTPS image fetches for inline images you reference by URL in your own Markdown.

Demos

A short loop through EXAMPLE.md on each platform — parsing, syntax highlighting, scrolling, theme toggle.

md.too rendering EXAMPLE.md on iOS
iOS
md.too rendering EXAMPLE.md on macOS
macOS

Source code

src/ is the whole codebase: 33 hand-written Swift files plus a bundled highlights.ini. No SPM packages, no CocoaPods, no vendored sources. Every file depends only on files in lower layers — the dependency graph is a tree, not a web, and zero #if os(...) walls remain anywhere in the source.

The layout is layered. A given file references only symbols from files in layers below it, so reading top-down or bottom-up never requires holding a cycle in your head.

Layer 0 — leaves, no in-module deps:

  • Platform-macOS.swift, Platform-iOS.swift — typealiases (PlatformFont, PlatformColor, PlatformImage), every platform* helper (font traits, colors, decoding, clipboard, light-appearance, PDF thumbnails). @_exported import AppKit/UIKit so consumers don't restate the framework dependency. Re-routes through a pdfDataExporter hook so CopyPdfProvider can sit here without dragging in apps-only code.
  • TeX.swift — splits $…$ / $$…$$ out of a line, renders inline maths as Unicode, and chooses between the two engines.
  • KaTeX.swift — the display-maths layout engine: lexer, parser, TeXbook Appendix G box layout, drawn through CoreText.
  • FileWatcher.swift — NSFilePresenter wrapper for live file-on-disk reload.
  • Environment.swift — pure SwiftUI primitives: PrefetchedImagesKey, SecondaryTextKey, ThemeMode, ThemeButton, SourceButton.

Layer 1 — parser, fonts, highlighter:

  • Style.swift — MarkdownStyle, the one value every size and spacing derives from: body, code and six heading sizes, block and paragraph spacing, list indent, code padding, all as fractions of the body size so zoom and Dynamic Type move the page together.
  • FontRole.swift — FontRole enum mapping .body / .heading(n) / .mono to a PlatformFont through the style.
  • MarkdownParser.swift — Block enum, ListItem, and the block parser with CommonMark container model + link-reference rewrite. Pure data; no SwiftUI.
  • Highlight.swift — regex syntax highlighter driven by highlights.ini.

Layer 2 — shared data helpers and selectable text:

  • TableMetrics.swift — column-width math (counts, char widths, point widths with optional minimums, longest-word, monospace serialization). Used by the on-screen table view, the PDF table renderer, and the HTML/plain exports.
  • ImagePrefetch.swift — walks [Block] and concurrently fetches remote image URLs.
  • SelectableText.swift — SelectableText SwiftUI view + NativeText builder + the AtomicKind / atomicKindKey / atomicIdKey / atomicCopyKey constants that mark code / table / image runs, and the applyIncremental splice that preserves selection across live reloads.
  • Find.swift — MarkdownFindController + FindableTextView protocol + markdownFindRanges; Find / Find Next over the single-surface document view, scrolled by aligning the match's fraction of the document to the same fraction of the viewport.

Layer 3 — bridges and pure-data exporters:

  • Bridges-macOS.swift, Bridges-iOS.swift — NS/UIViewRepresentable for NativeText, plus WindowAppearanceApplier. macOS variant also carries the anchor-scope selection arbiter that snaps a drag to whole when it crosses an atomic block (code, table, image).
  • HtmlExport.swift — [Block] → self-contained HTML with inline styles.
  • PlainExport.swift — [Block] → Markdown-flavored plain text, tables width-aligned.

Layer 4 — block views:

  • BlockViews.swift — per-block SwiftUI render (heading, list, code, table, image, quote, rule). TableBlock handles per-column natural-width measurement and graceful overflow.

Layer 5–6 — single-surface and PDF (apps only):

  • DocumentText.swift — document-wide NSAttributedString builder that backs continuous cross-block selection on the single-surface code path.
  • DocumentText-macOS.swift, DocumentText-iOS.swift — platform-specific table layout via extension DocumentText { static func table(...) } (NSTextTable on macOS, tab-stop paragraphs on iOS).
  • PDFExport.swift — entry layer: TempPDFs, prefetchDocumentImages, exportPDF / exportPDFDataSync, and the PDFExport enum that sets up the CGContext and drives the renderer.
  • PDFRenderer.swift — the PDFRenderer class itself: a CT-based page-by-page composer with embedded images, syntax-highlighted code, zebra tables, page numbers.

Layer 7 — toolbar:

  • Toolbar.swift — CopyDocButton (multi-flavor pasteboard with lazy PDF) and ShareButton. Apps only.
  • Toolbar-macOS.swift — Save-as-PDF and Save-as-HTML panels. macOS app only.

Layer 8 — top-level views:

  • MarkdownView.swift — chrome-less rendering core that routes between source / rendered / single-surface and prefetches inline images. Apps only.
  • QuickLook.swift — QLPreviewingController plus QLContent, a stripped preview view. Quick Look extension only.

Layer 9 — chrome:

Layer 10 — entry points:

  • MarkdownDocument.swift — MarkdownDocument: FileDocument, the UTType-aware document model that DocumentGroup (macOS) and .fileImporter (iOS) bind to. Apps only.
  • App-macOS.swift — @main, DocumentGroup, AppDelegate, Open-panel seeding. Wires pdfDataExporter back to exportPDFDataSync so the layer-0 CopyPdfProvider can deliver lazy PDF to the pasteboard. macOS app only.
  • App-iOS.swift — @main, WindowGroup, IOSDocumentRoot (file importer). iOS app only.

config/ holds the three Info-*.plist files for the app and extension targets, four *.entitlements files (release + debug pairs for the apps and the extension), and Base.xcconfig / version.xcconfig / a gitignored per-developer Local.xcconfig.

License

MIT.

About

Less is more. Holds for Markdown too.

Resources

Stars

10 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages