Domain Entities: Unit 2 — Document Management

MarkdownDocument (NSDocument)

The document model for Shoe Choo, subclassing NSDocument to leverage macOS native document architecture (auto-save, Versions, Recent Files).

class MarkdownDocument: NSDocument {
    var sourceText: String                      // Raw Markdown source
    var viewModel: EditorViewModel?             // Attached editor (created on makeWindowControllers)

    // NSDocument overrides
    override func read(from data: Data, ofType typeName: String) throws
    override func data(ofType typeName: String) throws -> Data
    override func defaultDraftName() -> String

    // Asset management
    func assetsDirectoryURL() -> URL?           // .md file's sibling assets/ directory
    func ensureAssetsDirectory() throws         // Create assets/ if needed
}

DocumentState

Tracks the lifecycle state of a document within the app.

enum DocumentState: Equatable {
    case blank                                  // New untitled document, never saved
    case saved(url: URL)                        // Saved to disk at known URL
    case edited                                 // Has unsaved changes (NSDocument.isDocumentEdited)
    case conflict                               // Save conflict detected (NSDocument ubiquity)
}

EditorSettings (@Observable Singleton)

Global user preferences for the editor, persisted via UserDefaults.

@Observable
final class EditorSettings {
    static let shared = EditorSettings()

    var fontFamily: String                      // e.g., "SF Mono", "Menlo"
    var fontSize: CGFloat                       // Points, e.g., 14.0
    var lineSpacing: CGFloat                    // Multiplier, e.g., 1.4
    var appearanceOverride: AppearanceMode      // .system, .light, .dark
    var defaultFocusMode: Bool                  // Default state for new documents
    var defaultTypewriterScroll: Bool           // Default state for new documents

    private init()                              // Singleton — load from UserDefaults
    func save()                                 // Persist to UserDefaults
    func reset()                                // Restore factory defaults
}

AppearanceMode

enum AppearanceMode: String, CaseIterable, Codable {
    case system                                 // Follow macOS appearance
    case light                                  // Force light
    case dark                                   // Force dark
}

EditorSettingsKey

UserDefaults keys for EditorSettings persistence.

enum EditorSettingsKey: String {
    case fontFamily         = "editor.fontFamily"
    case fontSize           = "editor.fontSize"
    case lineSpacing        = "editor.lineSpacing"
    case appearanceOverride = "editor.appearanceOverride"
    case defaultFocusMode   = "editor.defaultFocusMode"
    case defaultTypewriterScroll = "editor.defaultTypewriterScroll"
}

EditorViewModel (@Observable)

Per-document view model bridging the document model, parser pipeline (Unit 1), and the view layer.

@Observable
class EditorViewModel {
    // State
    var sourceText: String                      // Synced with MarkdownDocument.sourceText
    var nodeModel: EditorNodeModel              // Block tree from Unit 1 parser
    var cursorPosition: Int                     // UTF-16 offset
    var activeBlockID: EditorNode.ID?           // Currently active block
    var isFocusModeEnabled: Bool
    var isTypewriterScrollEnabled: Bool

    // Document binding
    weak var document: MarkdownDocument?        // Back-reference for save coordination

    // Methods
    func textDidChange(_ newText: String, editedRange: NSRange)
    func cursorDidMove(to position: Int)
    func attributedStringForDisplay() -> NSAttributedString
    func rerenderBlock(_ blockID: EditorNode.ID)
    func toggleFocusMode()
    func toggleTypewriterScroll()
    func insertImage(url: URL)
    func exportHTML() -> String
    func exportPDF() -> Data
}

RecentFileEntry

Represents a recently opened file for the File > Open Recent menu. Managed by NSDocumentController automatically; this entity is for internal tracking if needed.

struct RecentFileEntry: Identifiable, Codable {
    let id: UUID
    var url: URL                                // File URL
    var lastOpened: Date                        // Last access timestamp
    var displayName: String                     // File name without extension
}

FileService (Actor)

Low-level async file I/O service, isolated via Swift actor for thread safety.

actor FileService {
    func createDirectoryIfNeeded(at url: URL) throws
    func fileExists(at url: URL) -> Bool
    func safeWrite(data: Data, to url: URL) throws   // Atomic write via temporary file
}

ToolbarItem

Represents a toolbar button in the editor toolbar.

struct ToolbarItem: Identifiable {
    let id: String                              // e.g., "bold", "heading1"
    var label: String                           // Display label
    var systemImage: String                     // SF Symbol name
    var shortcutHint: String?                   // e.g., "Cmd+B"
    var action: () -> Void                      // Callback
}

FontDescriptor

Describes a selectable font for the Preferences UI.

struct FontDescriptor: Identifiable, Hashable {
    let id: String                              // Font family name
    var displayName: String                     // Localized display name
    var isMonospaced: Bool                      // For code-friendly fonts
    var sampleText: String                      // Preview string
}

Factory Defaults

Setting Default Value Rationale
fontFamily “SF Mono” macOS system monospace, excellent readability
fontSize 14.0 Standard comfortable reading size
lineSpacing 1.4 Balanced density vs readability
appearanceOverride .system Respect user’s macOS setting
defaultFocusMode false Not all users want focus mode on launch
defaultTypewriterScroll false Opt-in feature