Skip to main content
MarkdownView is split into two independent modules that work together to render markdown efficiently. This separation allows you to parse markdown without pulling in UI dependencies, and enables flexible rendering pipelines.

Module overview

The library consists of two Swift Package Manager modules:

MarkdownParser

Converts markdown strings into an Abstract Syntax Tree (AST) using swift-cmark. Has no UI dependencies.

MarkdownView

Renders the AST into native views with syntax highlighting, math rendering, and interactive links.

MarkdownParser module

The MarkdownParser module is a pure Swift package that transforms markdown text into a structured AST representation. It has zero UI dependencies, making it suitable for server-side use, command-line tools, or any context where you need to parse markdown without rendering it.

Key responsibilities

  • Parse markdown using swift-cmark with GFM extensions
  • Preprocess LaTeX math expressions ($...$, $$...$$, \\(...\\), \\[...\\])
  • Generate typed AST nodes for blocks and inline elements
  • Support incremental parsing for performance optimization

Core types

The parser defines two main node types:
Sources/MarkdownParser/MarkdownBlockNode/MarkdownBlockNode.swift
public enum MarkdownBlockNode: Hashable, Equatable, Codable {
    case blockquote(children: [MarkdownBlockNode])
    case bulletedList(isTight: Bool, items: [RawListItem])
    case numberedList(isTight: Bool, start: Int, items: [RawListItem])
    case taskList(isTight: Bool, items: [RawTaskListItem])
    case codeBlock(fenceInfo: String?, content: String)
    case paragraph(content: [MarkdownInlineNode])
    case heading(level: Int, content: [MarkdownInlineNode])
    case table(columnAlignments: [RawTableColumnAlignment], rows: [RawTableRow])
    case thematicBreak
}
Sources/MarkdownParser/MarkdownInlineNode/MarkdownInlineNode.swift
public enum MarkdownInlineNode: Hashable, Sendable, Equatable, Codable {
    case text(String)
    case softBreak
    case lineBreak
    case code(String)
    case html(String)
    case emphasis(children: [MarkdownInlineNode])
    case strong(children: [MarkdownInlineNode])
    case strikethrough(children: [MarkdownInlineNode])
    case link(destination: String, children: [MarkdownInlineNode])
    case image(source: String, children: [MarkdownInlineNode])
    case math(content: String, replacementIdentifier: String)
}
Both node types conform to Hashable, Equatable, and Codable, making them easy to compare, store, and transmit.

Usage example

import MarkdownParser

let parser = MarkdownParser()
let result = parser.parse("""
# Hello World

This is **bold** text with `inline code`.
""")

print(result.document) // Array of MarkdownBlockNode
print(result.mathContext) // Dictionary of preprocessed LaTeX expressions

MarkdownView module

The MarkdownView module takes the AST from MarkdownParser and renders it into native UIKit/AppKit views. It handles all visual presentation, interaction, and platform-specific rendering.

Key responsibilities

  • Render AST nodes into NSAttributedString with rich formatting
  • Syntax highlight code blocks using tree-sitter parsers
  • Render LaTeX math expressions as inline images
  • Load and display inline images asynchronously
  • Handle text selection, link taps, and accessibility
  • Manage view lifecycle and layout

Core components

MarkdownTextView

The main view class that displays rendered markdown. Available on both iOS/visionOS (via UIKit) and macOS (via AppKit).
Sources/MarkdownView/MarkdownTextView/MarkdownTextView.swift
public final class MarkdownTextView: UIView { // or NSView on macOS
    public let textView: LTXLabel
    public var theme: MarkdownTheme = .default
    public var linkHandler: ((LinkPayload, NSRange, CGPoint) -> Void)?
    public var imageTapHandler: ((String, CGPoint) -> Void)?
    
    public func setMarkdown(_ content: PreprocessedContent)
    public func setMarkdown(string: String) // Auto-parses on background queue
}

LTXLabel

A custom text rendering view from the embedded Litext module that provides:
  • High-performance Core Text layout
  • Text selection with gesture support
  • Inline attachments for images and math
  • Custom drawing callbacks for complex rendering
  • VoiceOver accessibility
See Sources/Litext/LTXLabel/LTXLabel.swift:11 for the full implementation.

TextBuilder

Converts AST nodes into NSAttributedString with embedded views:
Sources/MarkdownView/MarkdownTextBuilder/TextBuilder.swift
final class TextBuilder {
    struct BuildResult {
        let document: NSAttributedString
        let subviews: [PlatformView] // CodeView, TableView, etc.
        let blockSegments: [NSAttributedString] // Per-block strings for diffing
    }
    
    func build() -> BuildResult
    func buildIncremental(changes: [ASTDiff.Change], cachedSegments: [NSAttributedString]) -> BuildResult
}
The TextBuilder supports incremental builds by reusing cached NSAttributedString segments for unchanged blocks. This makes streaming updates efficient.

Module independence

The two-module architecture provides several benefits:

Parse without rendering

import MarkdownParser // No UI frameworks imported

let parser = MarkdownParser()
let result = parser.parse(markdown)

// Send AST over network, store in database, etc.
let data = try JSONEncoder().encode(result.document)

Test parsing independently

func testParsing() {
    let result = MarkdownParser().parse("# Title")
    XCTAssertEqual(result.document.count, 1)
    if case let .heading(level, content) = result.document[0] {
        XCTAssertEqual(level, 1)
    }
}

Customize rendering

import MarkdownParser
import MarkdownView

let result = parser.parse(markdown)
let content = PreprocessedContent(parserResult: result, theme: .custom)
markdownView.setMarkdown(content)

Data flow

Here’s how data flows through the architecture:
The PreprocessedContent class (see Sources/MarkdownView/MarkdownTextBuilder/PreprocessedContent.swift:13) combines:
  1. AST blocks from the parser
  2. Syntax highlighting maps from tree-sitter (thread-safe, can run on background queue)
  3. Rendered math images from LaTeX (requires main thread for UIKit trait access)
  4. Image sources for async loading
This preprocessing step can be split across threads for optimal performance.

Platform support

Both modules support iOS 16+, macOS 13+, and visionOS 1+. The MarkdownView module uses conditional compilation to provide a unified API across UIKit and AppKit:
#if canImport(UIKit)
    public final class MarkdownTextView: UIView { ... }
#elseif canImport(AppKit)
    public final class MarkdownTextView: NSView { ... }
#endif
This allows you to write platform-agnostic code that works seamlessly across Apple platforms.

Build docs developers (and LLMs) love