Skip to content

Add AppKit paragraph rendering layer with full iOS parity - #106

Merged
Jun Yan (junyan72) merged 18 commits into
mainfrom
macos-paragraph-view
Jun 30, 2026
Merged

Jun Yan (junyan72) merged 18 commits into
mainfrom
macos-paragraph-view

Conversation

@junyan72

@junyan72 Jun Yan (junyan72) commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add macOS support to SwiftStreamingMarkdown with a full AppKit rendering layer, snapshot testing infrastructure, and sample app support.

Core rendering

  • ParagraphNSView (NSTextView-based) with full iOS parity: attributed text, fade-in animations via CADisplayLink, link handling, context menus, and intrinsic content sizing
  • ParagraphViewCache for macOS with clearCache() to prevent test pollution from the static singleton
  • LatexViewProvider macOS path using NSColor(name:) dynamic colors and intrinsicContentSize

Snapshot testing

  • Custom Snapshotting strategy using CGWindowListCreateImage instead of the default cacheDisplay(in:to:) — fixes garbled MTMathUILabel rendering in macOS snapshots
  • MacVariant with standard (800pt) and wide (1200pt) window sizes, light/dark color schemes
  • SnapshotTestCase extended with AppKit assert() overload and cache clearing in setUp
  • TableViewSnapshotTests made cross-platform (removed #if canImport(UIKit) wrapper)
  • All 60 macOS snapshot tests passing

Sample app

  • Added SwiftStreamingMarkdownSampleMac target in project.yml (macOS 15.0)
  • Cross-platform source changes: MDFont/MDColor usage, NSPasteboard/NSSavePanel, NSColor(name:) dynamic colors, conditional toolbar/navigation modifiers

Refs #104

Validation

  • xcodebuild test -scheme SwiftStreamingMarkdown -destination "platform=macOS" -skipMacroValidation -disable-concurrent-testing — 60 tests, 0 failures
  • xcodebuild build -scheme SwiftStreamingMarkdownSampleMac -destination "platform=macOS" -skipMacroValidation — build succeeded
  • Verified math rendering visually in macOS sample app (quadratic formula, integrals, E=mc², etc.)
  • macOS snapshots re-recorded with CGWindowListCreateImage — math formulas render correctly

OSS readiness

  • No secrets, internal URLs, private identifiers, or product-only service names were added.
  • Public docs, fixtures, or notices were updated if behavior or dependencies changed.
  • Third-party dependency changes (adds, removes, version bumps) are intentional and reviewed. (no dependency changes)
  • Streaming/incomplete markdown behavior remains covered by fixtures or tests.

Jun Yan and others added 9 commits June 24, 2026 14:00
Replace the macOS placeholder (monospaced Text) with a proper
NSViewRepresentable wrapping MTMathUILabel. iosMath already supports
macOS via its MTConfig.h typedef layer. Uses intrinsicContentSize
instead of sizeToFit since NSView lacks the latter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement ParagraphNSView (NSTextView subclass) with:
- Word-by-word streaming fade animation via CADisplayLink
- NSTextView delegate for link click handling
- Custom context menu support via NSMenu
- Appearance tracking via AppAppearance
- LatexViewProviderMac for inline math rendering
- InlineCitationViewProviderMac for citation attachments
- Size caching and intrinsic content size management
- View reuse cache (ParagraphNSViewCache)

Replace the macOS ParagraphView placeholder with a proper
NSViewRepresentable wrapping ParagraphNSView, mirroring the iOS
UIViewRepresentable structure (coordinator, size cache, animation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ct shared animation code

Move all paragraph-related files under UI/Paragraph/:
- UI/Paragraph/ParagraphView.swift (NSViewRepresentable + UIViewRepresentable)
- UI/Paragraph/ParagraphAnimation.swift (shared FadeAnimationData, easeOut, constants)
- UI/Paragraph/UIKit/ParagraphUIView.swift
- UI/Paragraph/UIKit/ParagraphUIViewCache.swift
- UI/Paragraph/AppKit/ParagraphNSView.swift
- UI/Paragraph/AppKit/ParagraphNSViewCache.swift

Extract duplicated animation code into ParagraphAnimation.swift:
- FadeAnimationData struct
- paragraphEaseOut() curve function
- ParagraphAnimationConstants (fadeInDuration, delayBetweenWordsRatio)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move UIViewRepresentable to UIKit/ParagraphView+iOS.swift and
NSViewRepresentable to AppKit/ParagraphView+macOS.swift. Suffixes
avoid SPM duplicate filename errors within the same target.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add AppAppearance.update(style:) for UIKit and
AppAppearance.update(appearance:) for AppKit, encapsulating the
platform conversion logic. Update all callers in ParagraphUIView
and ParagraphNSView to use the new convenience methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge LatexViewProvider (iOS) and LatexViewProviderMac into one class
at UI/Paragraph/LatexViewProvider.swift. Platform differences are
handled with minimal #if blocks for the init parentView type and
sizeToFit vs intrinsicContentSize.

Also consolidate the LatexAttachmentData.resolvedTextColor extension
into the same file with a single computed property using MDColor.

Add comment to AppAppearance.update(appearance:) explaining that
bestMatch handles all dark/light variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…yling

- Remove InlineCitationViewProviderMac (no-op provider, citations use
  image-based rendering via InlineCitationAttachment.image)
- Remove its UTType.url registration from setupView()
- Remove empty configureVisualStyling placeholder on iOS

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…aphViewCache

Single shared file at UI/Paragraph/ParagraphViewCache.swift with #if
blocks only for the platform view type. Same API, same logic, one
class name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a macOS AppKit paragraph rendering implementation (NSTextView-backed) intended to match the existing iOS paragraph renderer’s behavior, while reorganizing paragraph-related code into a cleaner platform-split structure and extracting shared animation / LaTeX attachment logic.

Changes:

  • Added AppKit paragraph rendering (ParagraphNSView + cache + NSViewRepresentable wrapper) with animation, link handling, context menus, and accessibility.
  • Extracted shared paragraph animation primitives and unified LaTeX attachment view-provider code for iOS/macOS.
  • Refactored appearance updates and introduced view reuse caches for paragraph views.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Sources/MarkdownText/Utilities/AppAppearance.swift Adds platform-specific helpers to update cached light/dark appearance.
Sources/MarkdownText/UI/Paragraph/UIKit/ParagraphView+iOS.swift Cleans up platform gating and keeps iOS ParagraphView as a UIKit representable.
Sources/MarkdownText/UI/Paragraph/UIKit/ParagraphUIViewCache.swift Introduces a simple reuse pool for ParagraphUIView.
Sources/MarkdownText/UI/Paragraph/UIKit/ParagraphUIView.swift Moves shared animation / LaTeX logic out; uses centralized appearance update + shared animation constants.
Sources/MarkdownText/UI/Paragraph/ParagraphAnimation.swift New shared constants + fade animation data + ease-out curve.
Sources/MarkdownText/UI/Paragraph/LatexViewProvider.swift New cross-platform NSTextAttachmentViewProvider for LaTeX attachments.
Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphView+macOS.swift New macOS NSViewRepresentable wrapper around ParagraphNSView with size caching.
Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphNSViewCache.swift New reuse pool for ParagraphNSView.
Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphNSView.swift New AppKit NSTextView subclass implementing rendering/animation/menu/accessibility features.

Comment thread Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphNSView.swift
Comment thread Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphNSView.swift Outdated
Comment thread Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphNSView.swift Outdated
Comment thread Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphView+macOS.swift
Jun Yan and others added 6 commits June 25, 2026 11:04
Introduce MDParagraphView (ParagraphUIView on iOS, ParagraphNSView on
macOS) following the same pattern as MDFont/MDColor/MDImage. This
eliminates all #if blocks from ParagraphViewCache.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create MacVariant with window-size-based configs (narrow/standard/wide)
- Make SnapshotTestCase.assert() compile on both platforms using
  NSHostingController on macOS, UIHostingController on iOS
- Guard DeviceVariant and ViewImageConfig with #if canImport(UIKit)
- Wrap iOS-only test files with #if canImport(UIKit) so the test
  target compiles on macOS (parser/rewriter tests run on both)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add .macOS(.v14) to Package.swift platforms array, officially declaring
macOS support for build and test.

Add MarkdownTextMacSnapshotTests with 3 tests (lists, code blocks,
tables) × 4 variants (narrow/standard × light/dark) = 12 reference
snapshots recorded for macOS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove #if canImport(UIKit) guards from snapshot tests that don't
actually need UIKit (BlockQuoteView, MarkdownText, OrderedList,
UnorderedList). These tests now run on both iOS and macOS using the
platform-appropriate variant (DeviceVariant on iOS, MacVariant on
macOS).

Delete the separate MarkdownTextMacSnapshotTests.swift — the existing
tests cover the same scenarios on both platforms.

Record 100 macOS reference snapshots across the shared test suites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename variant output from 'standard-light' to 'macOS-standard-light'
for clear platform distinction. Remove the narrow (400pt) variant —
standard (800pt) light/dark is sufficient for macOS coverage.

50 reference snapshots (down from 100).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add clearCache() to ParagraphViewCache and call it in SnapshotTestCase.setUp()
to prevent cached ParagraphNSView instances from leaking math attachment
subviews across tests. Re-record all macOS snapshots with clean cache.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rm TableViewSnapshotTests; sample app macOS support

- Replace cacheDisplay(in:to:) with CGWindowListCreateImage for macOS
  snapshot capture — fixes garbled MTMathUILabel rendering in snapshots
- Make TableViewSnapshotTests cross-platform (remove #if canImport(UIKit)
  wrapper, use MDColor/MDFont)
- Add macOS target to sample app (project.yml + cross-platform source)
- Re-record all macOS snapshots with correct math rendering

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread Sources/MarkdownText/UI/Paragraph/AppKit/ParagraphView+macOS.swift Outdated
Comment thread Tests/MarkdownTextTests/SnapshotTestFoundation/DeviceVariant.swift Outdated
- Add MDImage(sfSymbol:) convenience init for cross-platform SF Symbol usage
- Add Color.dynamic(light:dark:) and Color.systemBackground extensions to library
- Use NSAttributedString.isEqual(to:) in ParagraphView macOS Equatable
- Rename DeviceVariant to DeviceiOSVariant
- Use URL.fromMixedEncodingString in ParagraphNSView link handler
- Remove dead NSTextViewDelegate conformance from ParagraphNSView

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@junyan72
Jun Yan (junyan72) merged commit 8036746 into main Jun 30, 2026
4 checks passed
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.

2 participants