Changelog
The development story behind MarkeMark — every version, every decision.
Build 10 delivers the UX win Build 7 promised — inline images just render everywhere, no banner, no folder prompt — without Build 7's Quick Look regression. Sandbox comes off the main app only; the Quick Look extension stays sandboxed (which macOS requires for extension registration). Open any .md file and its sibling images render immediately. Spacebar-preview any file in a folder you've already touched and its images render there too.
- No more "Grant Folder Access…" banner. The main app reads sibling images directly —
![[yoga.jpg]]andjust work, regardless of where the.mdfile lives - Quick Look shows inline images for any
.mdfile in a folder you've opened at least once in the main app — opening any file in a folder kicks off a background walk that pre-caches every image for the Quick Look extension to read - First-time access to
~/Desktop,~/Documents, and~/Downloadssurfaces a standard macOS privacy prompt. Approve once per folder; after that, MarkeMark reads those locations transparently - Mac App Store eligibility remains off the table, same as Build 7. Distribution continues via notarized DMG from the website
com.apple.security.app-sandbox removed from markemark.entitlements (main app) only; MarkeMarkQuickLook.entitlements keeps its sandbox, which is what pluginkit needs for QL extension registration (Build 7's regression). pbxproj flipped ENABLE_APP_SANDBOX to NO and dropped ENABLE_USER_SELECTED_FILES for the main-app build configs; QL extension build settings unchanged. Main app Info.plist gains NSDesktopFolderUsageDescription, NSDocumentsFolderUsageDescription, NSDownloadsFolderUsageDescription, NSRemovableVolumesUsageDescription, NSNetworkVolumesUsageDescription — these TCC prompts are no-ops for sandboxed apps (sandbox denies before TCC gets a chance, per Apple Developer forum guidance) but do fire for non-sandboxed apps, which is the regime we're in now. ContentView.DocumentURLReader.onURL kicks off ImageCache.populateFromFolder on every new document URL, caching the whole folder's images into the app-group container so the sandboxed Quick Look extension hits on first preview of any sibling doc. The banner UI (folderAccessBanner, missingImageCount, grantFolderAccess) is removed from ContentView; FolderAccessManager is now unused in the main app and retained as dead code for one build so rollback stays a one-commit revert — will be deleted in Build 11. release-app.sh unchanged — step 5b re-signs each target with its own entitlements file, which correctly drops sandbox on main and keeps it on the appex.
Quick Look was silently broken after Build 7. Spacebar-previewing a .md file in Finder fell through to the plain-text system generator — raw markdown source, no rendering, no images. Build 9 puts the sandbox back on both the app and the Quick Look extension, which is what QL needs to register at all. The "Grant Folder Access…" banner returns alongside it; the tradeoff Build 7 tried to make turned out to cost the feature that matters most.
- Quick Look is working again. Spacebar on a
.mdfile shows the MarkeMark-rendered preview (headings, code, tables, checklists, images from previously-opened vault folders) - App Store eligibility is restored — sandbox is back on with no extra exceptions
- The "Grant Folder Access…" banner returns when a document has images MarkeMark can't read. One-click grant per folder, same UX as v1.6 Build 1
com.apple.security.app-sandbox drop silently un-registered the .appex with pluginkit — Apple's QuickLook preview extension host refuses unsandboxed extensions. The LaunchServices record for the installed app carried no plugin Identifiers: line, and pluginkit -m -p com.apple.quicklook.preview omitted MarkeMark entirely, so Finder fell through to /System/Library/QuickLook/Text.qlgenerator. Survey of every third-party QL preview extension in /Applications on this machine: all had com.apple.security.app-sandbox = 1; MarkeMark post-Build-7 was the only outlier. Fix reverts both entitlements files to their pre-Build-7 contents: QL extension gets app-sandbox + application-groups + files.user-selected.read-only; main app gets app-sandbox + print + files.user-selected.read-write + application-groups. The release-app.sh step 5b re-sign remains necessary and keeps doing the right thing — with sandbox re-enabled, it's a no-op drift guard instead of a drift remover. The ImageCache app-group bridge built in v1.6 Build 1 still carries Quick Look: once the main app reads an image (via bookmark grant or per-doc user-selected.read-write), the bytes are cached into the app-group container where the sandboxed QL extension can read them directly without its own file-access entitlement ever needing to resolve.
Scroll-lock moved off the toolbar (it was pushing the view-mode trio off centre) and back onto the pane divider — this time near the bottom, where it's out of the way while still being a natural home for a pane-sync control.
- Scroll-lock is now a small lock button sitting near the bottom of the split-pane divider. View-mode buttons in the toolbar are centred again
- No functional change: ⇧⌘Y still toggles,
lock/lock.openstill reflect the state
SplitPaneView from Build 5's commit (200399c) — two panes with a dividerOverlay ViewBuilder slot and @SceneStorage("splitFraction") per-window persistence. Lock button is bottom-aligned inside that slot via VStack { Spacer(); Button(…).padding(.bottom, 20) } so it hugs the bottom of the visible divider regardless of window height. Toolbar-side scroll-lock button deleted; view-mode trio is the only content left in the toolbar group, restoring its natural centring.
MarkeMark now reads sibling images — both in the main editor preview and in Finder's Quick Look — with no folder-access prompt. Open any .md file that references images in the same folder and they just render. No more "Grant Folder Access…" banner, no more Quick Look previews with broken images for files you haven't opened yet.
- Quick Look renders inline images universally — every
.mdfile preview shows images, regardless of whether you've opened the file in the main app first or granted folder access - No more "Grant Folder Access…" banner in the main editor. Image reads for siblings of the open doc just work
- Trade-off: MarkeMark's macOS app sandbox is now disabled on both the main app and the Quick Look extension. MarkeMark can read any file your user account can read — same as Obsidian, VS Code, and most other Developer-ID-distributed macOS apps. If we ever want to ship to the Mac App Store, this will need to be reverted; the sandbox removal is tracked in the entitlements files so retracing is a one-line flip
com.apple.security.app-sandbox removed from both markemark.entitlements and MarkeMarkQuickLook.entitlements, along with the now-meaningless files.user-selected.read-write / read-only and print entitlements. com.apple.security.application-groups retained — the app-group container still backs ImageCache as a perf path, and FileManager.containerURL(forSecurityApplicationGroupIdentifier:) requires the entitlement even on unsandboxed processes. The existing FolderAccessManager bookmark machinery and the "Grant Folder Access" banner are now dead code paths (they'd only fire if Data(contentsOf:) somehow still failed, which for real-world paths it won't) — left in place as a no-op safety net and for easy restoration if the sandbox ever comes back. ImageInlinerQL's "try direct disk read first, fall back to app-group cache" order now succeeds on the direct read, skipping the cache lookup; the cache path stays in case the QL extension is ever re-sandboxed. Notarization and Developer ID signing are unaffected — hardened runtime still applies (the -o runtime codesign flag), Developer ID distribution accepts sandboxed and unsandboxed bundles alike.
Two fixes on top of Build 5. The lock moved to the toolbar (bigger, easier to hit) and a rendering glitch where bits of ![[yoga.jpg]] syntax leaked into the preview as visible text is squashed.
- Scroll-lock is now a larger lock button in the top toolbar, right of the view-mode trio. The divider version is gone — toolbar placement keeps the icon at a consistent, high-visibility location regardless of where the divider has been dragged
- Fixed a preview glitch where Obsidian image embeds (
![[foo.jpg]]) sometimes left a strayfoo.jpg">fragment visible below the rendered image - Dropped the custom
SplitPaneView— reverted to the nativeHSplitViewnow that no content needs to sit on the divider
processWikiLinks matching [[foo.jpg]] inside ImageInliner's emitted <img … data-original-src="![[foo.jpg]]">. The wiki-link regex was scoped to skip <code>/<pre> contexts but not arbitrary HTML tag attribute contexts, so the [[…]] in the attribute got hijacked into an anchor, breaking the attribute string and leaking fragments to the DOM. Fix: inside-open-tag detection via last < vs last > position in the pre-match text — if the last < has no matching > before the match, we're inside an attribute list and skip. Same guard should probably land on other block/inline passes eventually, but this is the only one Adam has hit in practice and the others don't match [[…]]. Toolbar button is a standalone Button with Image(systemName:) at 15 pt vs the 12 pt view-mode glyphs, in a 30 × 24 frame (vs 26 × 22) so the size difference reads as deliberate.
UX polish on the scroll-lock toggle introduced in Build 4. The link icon in the toolbar was using an SF symbol that doesn't exist on macOS 13, so the button vanished when clicked. Moved to a lock icon on the divider between the panes — a more natural spot for a pane-sync control.
- The scroll-lock control is now a small lock button sitting on the split-pane divider, not in the toolbar. Click it to lock (panes scroll together) or unlock (panes scroll independently) — the icon switches between
lockandlock.openso the state is legible at a glance - Fixed the disappearing icon. The previous
link.badge.minussymbol isn't available on macOS 13, which is why the button vanished in the off state
SwiftUI.HSplitView doesn't expose its divider for custom content, so the .split case now renders through a small new SplitPaneView — two panes, a 1 px visual hairline over an 8 px draggable hit zone, and a dividerOverlay ViewBuilder slot for the lock button. Split fraction is persisted via @SceneStorage("splitFraction") so each window remembers its own geometry across relaunches. The drag gesture captures the fraction at drag start so translation is applied relative to the grab point rather than the current clamped position, which otherwise drifts during a continuous drag against the min-width clamps. Icon choice avoids any SF Symbol introduced after macOS 13 — lock / lock.open are OS-baseline and can't render blank on older targets.
Based on hands-on testing of Build 3: the pane-to-pane selection overlay was still "weird" even after follower-only logic and edit-driven clears — so it's gone. And a new scroll-lock toggle lets you decouple the two panes when a tall image pushes text out of view.
- The pane-to-pane selection overlay is removed. The translucent band that tried to mirror your editor selection over to the preview (and vice versa) was too coarse to be useful — highlighting one word marked an entire paragraph. The blinking sync caret stays
- New toolbar button and View-menu toggle: Sync Scroll Between Panes (⇧⌘Y). Turn it off to scroll each pane independently — useful when a tall inline image on one side hides the text you're editing on the other
- Image-heavy docs open faster. The shared app-group image cache that powers Quick Look previews now writes off the main thread and skips redundant writes when the bytes on disk are already up to date — first paint no longer waits on dozens of atomic disk writes
SelectionOverlayView (an NSView subview added to the NSTextView), PreviewActions.showSelectionOverlay/hideSelectionOverlay, the SELECTION: bridge message, the #selection-overlay <div> and its CSS/JS (_applySelectionOverlay, _selectionOverlayRaf, etc.), and the wire-up through ContentView.handleEditorSelection/handlePreviewSelection. The sync caret (SyncCaretView + #sync-caret) stays — it's precise, source-line-based, and fast. Scroll-lock is a plain @AppStorage("scrollSyncEnabled") bool gating both handleEditorScrollProportion and handlePreviewScrollProportion; caret mirroring is left on because it's discrete-event-driven, not a per-frame driver of the viewport, so it doesn't fight the "let each pane scroll on its own" intent. ImageCache.store(…) now dispatches to a utility-QoS serial queue and compares the to-be-written source mtime against the existing .meta plist's mtime key — if they match within 1 s, the write is skipped entirely. The Quick Look extension is the only consumer of the app-group cache and it runs only when the user QL-previews a file, so there's no in-session reader that needs the write to have landed synchronously.
Follow-up polish based on hands-on feedback from Build 2 — scroll now feels smooth through big images, and the selection overlay behaves sensibly instead of stacking.
- Scroll sync is now proportional — whatever percent of the way down you are on one pane, the other matches. No more leaps when a tall image enters the viewport
- The selection overlay now lives on the non-focused pane only. No more two overlays stacking when you click between panes — the overlay clears on focus change, so you always see exactly one highlight
- Selection overlays clear on text edits. After typing or deleting, the next selection event redraws cleanly instead of pointing at stale source lines
SCROLL_LINE carrying a fractional line number) with SCROLL_PROP carrying a 0–1 proportion of each pane's scrollY / maxScroll. Motion through asymmetric content — one editor line mapping to a 600 px image — is now linear rather than piecewise-interpolated, so the classic "24 px editor scroll → 300 px preview scroll while inside a tall block" jump is gone. EditorActions.scrollToProportion and PreviewActions.scrollToProportion are the Swift-side entry points; computeScrollProportion / scrollPreviewToProportion are the JS-side. The velocity-clamp catch-up loop from Build 2 stays, now acting on proportion-mapped Y values. Follower-only overlay logic gates on previewHasFocus in ContentView — handleEditorSelection no-ops when preview is the leader and vice versa, and handleFocusChange wipes both overlays on every focus flip so the old follower's stale mirror doesn't persist. .onChange(of: document.text) calls clearSelectionOverlays() and the innerHTML-swap JS pipes hideSelectionOverlay() after the DOM replacement to prevent pixel-pinned overlay positions from lingering over re-laid-out blocks.
Split-pane polish — images now show up on first open, scrolling feels smooth through tall blocks, and your selection is mirrored across panes so you can see what's about to be affected.
- Inline images render on first open in preview — no more flipping view modes to coax them to appear
- Scrolling the editor through a tall image or wide table no longer makes the preview leap in huge chunks. The follower eases toward its target over a few frames instead of snapping
- Highlighting text in either pane now shows a translucent band covering the corresponding blocks in the other pane — useful for knowing exactly what your next Delete or Cmd+X will affect
loadHTMLString vs evaluateJavaScript ordering bug — the initial render fires from .onAppear before DocumentURLReader's KVO on window.representedURL populates documentURL, so ImageInliner returns the markdown unchanged (no base64), then the URL-arrival re-render tries to patch the DOM via innerHTML before the in-flight loadHTMLString has finished parsing. Fix: a pageReady flag on Coordinator, flipped by a new WKNavigationDelegate.webView(_:didFinish:) handler, gates the inner-HTML path so any HTML update arriving while the page is still loading triggers a fresh loadHTMLString instead. Scroll smoothing is a velocity clamp in the preview's _applyPreviewScroll: cap each frame's step at max(40, |delta| × 0.35) px with a catch-up requestAnimationFrame loop that closes the gap; small deltas (normal prose scrolling) are unaffected, large deltas converge in ~10 frames. Selection mirroring is an overlay-only implementation — the editor/preview each report their selection range as (startLine, endLine) source-line pairs over the existing bridge, and the follower pane draws a semi-transparent NSView / <div> spanning the block range. No attempt to hold a real DOM selection in both panes at once (which would fight native focus).
Obsidian-style vaults now render inline images — in the main editor's preview and in Finder's Quick Look. Drop a ![[screenshot.png]] or  and the image shows up.
- Inline images for
and Obsidian's![[file]]embed — works in the preview, HTML export, PDF export, print output, and Quick Look - A one-time "Grant Folder Access…" banner appears if your vault lives outside the sandbox. Grant the root folder once and every file inside renders its images
- Your
.mdfile stays clean on save — images render as inline base64 but round-trip back to their original/![[file]]syntax, never ballooning the source
<group-container>/image-cache/), not through security-scoped bookmarks. URL(resolvingBookmarkData:, options: .withSecurityScope, …) consistently fails in sibling sandboxed extensions despite matching team ID and application-groups entitlements — App Groups are a reliable data sharing mechanism, bookmark blobs are not. The main app writes through on each successful disk read in ImageInliner and runs an eager prefetch pass when the user grants folder access. Round-trip safety comes from a data-original-src="references/foo.png" attribute stamped onto every generated <img> tag, preferred over the base64 src during HTML→Markdown conversion so the original path (or Obsidian embed syntax) is preserved verbatim on save.
Pane sync gets polished — smoother scrolling, crisper caret blink, and a more precise indicator that tracks where you're actually editing. Plus wiki links now render correctly in Finder's Quick Look previews.
- Scroll sync feels smoother across the bridge — Swift-side coalescing limits preview updates to one per run-loop pass instead of saturating the WebKit IPC channel
- The sync-caret blink is now a crisp on/off, matching a native text caret — no more fade-in-fade-out shimmer
- The sync-caret now tracks your position within a paragraph, not just which paragraph you're in. Edit mid-block on one side and the other side's indicator lands near the same word rather than snapping to the block's first line
- Wiki links (
[[page]],[[page|alias]]) now render as dark-purple underlined text in Finder's Quick Look previews too — the main app and the Quick Look extension finally agree
PreviewActions now queues pending scrollToSourceLine / showSyncCaret targets and flushes them together via DispatchQueue.main.async — at most one evaluateJavaScript call per run-loop pass, regardless of how fast NSClipView fires boundsDidChange. The blink uses step-end keyframes with visibility: hidden (not opacity: 0) so the GPU can't interpolate; the editor-side CAKeyframeAnimation was simplified from four-value-with-duplicate-keytimes to a clean two-value discrete blink. Caret precision within a block is a fractional line report — editor sends lineNumber + charOffset/lineLength, preview interpolates using the block's own getBoundingClientRect().height instead of the inter-block gap. The Quick Look extension's MarkdownRendererQL pre-processes [[wiki]] syntax into inline <a class="internal-link"> anchors before handing off to swift-markdown, which passes raw inline HTML through unchanged.
The two panes now scroll together. Drag either side and the other follows to the same spot in your document — no more hunting for your place after a long scroll.
- Scroll the editor, the preview follows. Scroll the preview, the editor follows. They stay aligned to the block you're looking at
- No more jumping to the top of the left pane when you edit something on the right — your scroll position is preserved across round-trip updates
- Ping-pong-proof: when one pane drives the other, the reaction doesn't bounce back
data-source-line="N" attribute pointing to its 0-indexed line in the markdown source. The preview's JS reports the source line of the first visible anchor on scroll; the editor's NSScrollView clip-view bounds observer does the same via the layout manager's glyph-to-line lookup. Each side uses a 250 ms ignore window after receiving a programmatic scroll so the resulting echo doesn't re-trigger a round-trip. The full-text-replace path in EditorView.updateNSView also now saves and restores the clip view origin, fixing the older "jump to top when preview edit arrives" glitch. data-source-line is stripped in stripBrowserFormattingTags so it never leaks into the markdown on the way back.
Three round-trip corruption bugs squashed. Editing in the preview pane no longer silently rewrites your markdown.
- Code block language (e.g.
```swift) no longer gets wiped every time you edit the preview - Footnote references and definitions survive preview edits intact
- Bare URLs stay bare — no more automatic
[url](url)bracketization on every round-trip
stripBrowserFormattingTags was removing class and id attributes globally before convertCodeBlocks and convertFootnotes ran, so those converters never saw the metadata they needed (class="language-swift", id="fn-1", class="footnotes"). Fix: move the two metadata-dependent converters ahead of the stripper. Separately, the code-block regex was widened to tolerate highlight.js's hljs class and to strip its token <span>s from the captured content. The bare-URL fix just checks whether the display text equals the href before emitting markdown.
Obsidian wiki links finally look like links — no more raw [[page|alias]] noise cluttering your printouts.
[[page]],[[page|alias]], and[[page#section|alias]]all render as subtle dark-purple underlined text- Display name mirrors Obsidian: alias wins if present, otherwise
page > sectionfor heading refs - Works in the preview, HTML export, PDF export, and print output
- Edits to the displayed link text in the preview pane now propagate back to the markdown as a new alias — no more lost keystrokes
data-wiki / data-wiki-alias attributes so edit detection can compare the current inner text against what the original display would have been, then promote any edit to an alias. Inline-markdown chars (*, _, ~, =, `, [, ]) inside wiki syntax are swapped for numeric HTML entities before the bold/italic/highlight/code/link passes see them, so an asterisk in a page name like [[*🧗 aExplore]] can't corrupt the surrounding HTML.
Printing, for real this time. Checklists, schedules, and reference sheets that actually fit on one page.
- Cmd+P now opens a print preview sheet with live column flow
- Choose 1–9 columns — perfect for fitting long checklists on a single page
- Portrait or landscape orientation with a one-tap toggle
- Zoom slider dials the font size up or down so you can nail the layout before printing
- Print to paper or save as PDF from the same sheet — your choice
- Native macOS print dialog with full access to your printer's settings
com.apple.security.print sandbox entitlement — the sandbox was silently blocking every print IPC call regardless of API surface. The print implementation itself was fine the whole time.
Formatting shortcuts finally work where you'd expect them to — in the preview pane.
- Formatting shortcuts (Cmd+B/I/K, Cmd+Shift+X, etc.) now work in the preview pane
- Focus-aware format routing — formatting applies to whichever pane has focus
- Fixed stray cursor artifact at the top of the preview on initial load
- Added
<strike>tag conversion for strikethrough round-trips - Interactive tutorial replaces the old welcome screen
PreviewActions bridge that calls document.execCommand() directly in the WebView, with focus tracking to route format actions to the correct pane.
The app can now tell you when a new version is available.
- Auto-checks for updates on launch (once per day, fails silently offline)
- "Check for Updates..." menu item for manual checks
- Automated release pipeline — build, sign, notarize, package DMG, and generate version manifest in one command
The first public release. Getting a macOS app from "works on my machine" to "works on anyone's machine" turned out to be its own project.
- Website and notarized DMG distribution
- Quick Look extension for previewing .md files in Finder
- Table rendering — supports minimal 1-dash separators, HTML-escapes cell content
- Clickable checkboxes in task lists — toggle directly in the preview
- Bundled highlight.js locally for fully offline syntax highlighting
QLIsDataBasedPreview, an SE-0409 import visibility change, and WKWebView's XPC sandbox incompatibility. Notarizing the DMG itself (not just the app inside) was also necessary to avoid Safari's quarantine dialog.
The release where MarkeMark went from a working prototype to something you'd actually want to use daily.
- Full GFM rendering: tables, task lists, nested lists, footnotes, highlights, auto-links
- Syntax highlighting with highlight.js (auto-detects language)
- Find bar (Cmd+F) with real-time match counting, next/previous, case sensitivity
- Export as HTML — self-contained file with all CSS and JavaScript inlined
- Undo/redo from either pane via EditorActions bridge
- Format shortcuts (Cmd+B/I/K) work from both editor and preview panes
After 4+ sessions and ~1,125 lines of code spent on scroll sync and selection mirroring between panes, the root design flaw became clear: two independent algorithms that had to agree on block counts, but couldn't — the markdown renderer and the DOM structure used fundamentally different counting. Every fix introduced a new edge case.
The solution was to delete all of it. Sync cursors, selection highlighting, scroll tracking, source-line annotation — all removed. Sometimes the best code is code you delete.
SyncCursorView, FocusTrackingTextView, ActivePane enum, data-source-line annotation system, and ~15 JavaScript functions for focus tracking, cursor synchronization, and scroll position mirroring.
Focused on making the editor feel responsive rather than adding new features.
- 250ms debounced editor-to-preview sync (no per-keystroke re-renders)
- Incremental DOM updates via innerHTML (preserves scroll position instead of full-page reload)
- WKWebView memory leak fix — proper
dismantleNSViewcleanup removes message handlers - View mode switching: Cmd+1 (preview only), Cmd+2 (editor only), Cmd+3 (split)
- PDF export
- Safe file saves —
guard/throwreplaces force-unwrap on encoding - Modern paste handler using Selection API (replaces deprecated
execCommand)
The starting point. One question: what if you could edit either side of a markdown editor?
- Split-pane layout with resizable divider
- Bidirectional editing — write markdown on the left, or edit the rendered preview on the right, and changes sync both ways
- Custom regex-based markdown renderer + HTML-to-markdown converter
- Browser formatting artifact cleanup (
stripBrowserFormattingTags) for clean round-trips - Dark mode support in both panes
- Open and save .md, .markdown, and .txt files