Skip to content

Swift + SwiftUI port of the app, Quick Look extension, and render helper - #16

Open
santiagobarros wants to merge 14 commits into
JackYoung27:mainfrom
santiagobarros:pr/swift-port
Open

Swift + SwiftUI port of the app, Quick Look extension, and render helper#16
santiagobarros wants to merge 14 commits into
JackYoung27:mainfrom
santiagobarros:pr/swift-port

Conversation

@santiagobarros

Copy link
Copy Markdown

What this is

A port of MDviewer's three native components to Swift + SwiftUI, Apple's recommended stack for new macOS apps — offered as a possible future direction for the project.

Stacked on #15 — this branch contains all of that PR's commits plus the swift/ directory; review #15 first, then this one shrinks to the Swift additions.

Structure

A Swift Package under swift/ (buildable with Command Line Tools alone — no Xcode project required, preserving this repo's script-driven ethos):

Target Replaces Notes
MarkdownViewerApp src/main.m SwiftUI DocumentGroup lifecycle: open panel, recents, and window-per-document come from the framework; PreviewModel owns the WKWebView, renderer pipeline, live reload with scroll restore, and all menu commands
QuickLookPreview src/quicklook.m Same data-based provider: JavaScriptCore marked + KaTeX, cid: images, Mermaid cache + XPC
RenderHelper src/render-helper.m Same on-demand launchd agent contract, including team-identifier XPC validation
RenderHelperKit src/render-helper.h Shared XPC protocol; @objc names match the ObjC build, so Swift and ObjC components interoperate during a transition

./swift/build.sh compiles the targets and swaps the binaries into the bundle produced by the root build.sh, which continues to own resources, vendored libraries, plists, entitlements, and signing. viewer.js / viewer.css are shared unchanged — the rendering pipeline is identical by construction.

Verification

  • Builds clean under both swift build and xcodebuild (Xcode 26).
  • The Swift app writes Mermaid SVG cache entries with byte-identical hashes to the ObjC build — either implementation can read the other's cache.
  • The Swift extension served real QLPreviewPanel sessions and reached the Swift helper over XPC for a never-before-seen diagram; no crash reports.

Known gaps (also in swift/README.md)

  • SwiftUI doesn't expose NSWindow.tabbingMode, so the tab-by-default preference is lost (manual tabbing still works).
  • Swift 5 language mode; Swift 6 strict-concurrency migration pending.
  • An Xcode project wrapper would still be needed for Mac App Store submission (provisioning/app groups); these sources carry over unchanged.

A stale copy merged over by ditto keeps files the new bundle no longer
ships, which breaks the code-signature seal and makes Gatekeeper reject
the app.
The README promises no network calls, but the app contacted the GitHub
releases API on every launch. The check now runs only from a new
"Check for Updates…" menu item, with explicit feedback for up-to-date
and unreachable cases.
New Settings window (Cmd+,) with three document fonts: the existing
serif, GitHub's system sans stack, and Geist (vendored from the geist
npm package with a pinned SHA-256 like the other libraries). The choice
persists in NSUserDefaults and is applied live to all open windows as a
data-font attribute driven by an injected user script.
A data-based QLPreviewProvider app extension built into
Contents/PlugIns by build.sh — no Xcode project required. Markdown
renders with the bundled marked.umd.js under JavaScriptCore; LaTeX math
is extracted from the source (code-fence aware, so $a_i$ survives
emphasis parsing) and rendered with katex.renderToString, which needs
no DOM — Quick Look executes no JavaScript in data-based previews.
KaTeX CSS ships with its woff2 fonts inlined as data URIs. Local images
referenced by the markdown are embedded as cid: attachments on the
QLPreviewReply, since data-based previews have no base URL; a read-only
filesystem exception lets the sandboxed extension read them (macOS
additionally asks once for privacy-protected folders like Desktop).
Mermaid fences degrade to a styled source block for now.

The appex is signed with its entitlements before the outer app —
codesign --deep would strip them. Note for testing: qlmanage -p crashes
on macOS 26 for all third-party Quick Look extensions; use a
QLPreviewPanel harness or Finder itself.
The Quick Look sandbox cannot host a browser engine (WebContent
processes are terminated immediately, and view-based preview extensions
are never launched by quicklookd — both verified on macOS 26), so
Mermaid is solved in two layers:

1. The app caches every SVG it renders, keyed by sha256 of the trimmed
   diagram source, in both light and dark themes (viewer.js renders the
   non-displayed theme in the background after the visible pass).
2. On cache miss the extension asks an optional on-demand launchd
   helper over XPC. launchd registers no running process at rest,
   spawns the helper when the extension connects, and it exits after
   45 seconds idle. It is opt-in (./install.sh --with-mermaid-helper)
   because launchd agents surface as Login Items.

The extension prefers the SVG matching the current system appearance,
then a live helper render, then a stale-theme SVG; unseen diagrams with
no helper degrade to the styled source fallback. A permissions section
in the README documents the model.
./build.sh installer builds a standard macOS installer whose customize
pane offers two optional selections: setting Markdown Viewer as the
default .md handler (pre-selected) and installing the Mermaid Quick
Look helper (off by default, since it registers a Login Items entry).
The choices run per-user postinstall scripts that call the same
set-default-handler.py and register-mermaid-helper.sh now shipped in
the app's Resources; install.sh reuses them instead of embedding its
own copies. The app package removes any previous install before laying
down the payload so stale files cannot break the signature seal.
build.sh auto-selects a signing identity — CODESIGN_IDENTITY override,
then Developer ID Application (with hardened runtime and secure
timestamp, the notarization prerequisites), then Apple Development,
then ad-hoc (unchanged default for contributors without certificates) —
and reports what it used. New ./build.sh notarize submits the release
zip via notarytool and staples the ticket; the installer is signed when
a Developer ID Installer certificate exists.

The render helper derives its own team identifier from its signature
and rejects XPC callers signed by any other team (macOS 13+
setCodeSigningRequirement). Ad-hoc builds carry no team, so local
development skips the check.
A Swift Package under swift/ (buildable with Command Line Tools alone,
no Xcode project) with four targets mirroring the ObjC architecture:
a SwiftUI DocumentGroup app, the data-based Quick Look extension, the
on-demand mermaid render helper, and a shared RenderHelperKit whose
@objc XPC protocol keeps Swift and ObjC components interoperable.

swift/build.sh reuses the root build.sh for bundle layout, vendored
libraries, plists, entitlements, and signing, then swaps in the three
Swift binaries — viewer.js/viewer.css remain the shared rendering
pipeline. The Swift app writes mermaid cache entries with hashes
identical to the ObjC build, so either implementation can read the
other's cache. Parity gaps are listed in swift/README.md.
@santiagobarros

Copy link
Copy Markdown
Author

Rebased onto the restructured #15 — this PR is now that branch plus a single commit containing the entire Swift port, so once #15 (or any prefix of it) lands, this diff reduces to just the swift/ directory.

On the first launch where another app (usually Xcode on a developer
Mac) owns Markdown files, ask once whether to make Markdown Viewer the
default, using NSWorkspace's modern default-application API. Declining
is remembered; if the app is already the default (e.g. the installer
checkbox or install.sh set it), the prompt never appears.
…sions

New optional, pre-selected choice: elects Markdown Viewer's Quick Look
extension over any other Markdown previewer already registered (e.g.
QLMarkdown), via pluginkit -e use, mirroring the existing default .md
handler choice. install.sh now does the same unconditionally, matching
its existing unconditional default-handler behavior.
…ages

Installer.app renders custom welcome/conclusion resources via the
legacy WebKit1 WebView class, which is deprecated (since macOS 10.14)
and crashes on current macOS when that code path is exercised — this
is what actually caused 'the installer says it failed' even though the
payload had already been applied. RTF resources are rendered by
TextKit directly, with no WebKit involved at all; confirmed via a
minimal repro that the HTML path invokes WebView while the RTF path
does not.
@santiagobarros
santiagobarros deleted the pr/swift-port branch July 11, 2026 19:57
@santiagobarros
santiagobarros restored the pr/swift-port branch July 11, 2026 19:58
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.

1 participant