Component Methods: Shoe Choo

Note: Method signatures define the interface. Detailed business rules will be specified in Functional Design (CONSTRUCTION phase).


C-02: MarkdownDocument

class MarkdownDocument: NSDocument {
    var viewModel: EditorViewModel

    // NSDocument lifecycle
    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?
    func ensureAssetsDirectory() throws -> URL
}

C-03: EditorViewModel

@Observable
class EditorViewModel {
    // State
    var sourceText: String
    var nodeModel: EditorNodeModel
    var cursorPosition: Int
    var activeBlockID: EditorNode.ID?
    var isFocusModeEnabled: Bool
    var isTypewriterScrollEnabled: Bool

    // Text editing
    func textDidChange(_ newText: String, editedRange: NSRange)
    func cursorDidMove(to position: Int)

    // Rendering
    func attributedStringForDisplay() -> NSAttributedString
    func rerenderBlock(_ blockID: EditorNode.ID)

    // Focus mode
    func toggleFocusMode()
    func toggleTypewriterScroll()

    // Image insertion
    func insertImage(at position: Int, relativePath: String)

    // Export
    func exportHTML() async throws -> String
    func exportPDF() async throws -> Data
}

C-04: EditorSettings

@Observable
class EditorSettings {
    // Typography
    var fontFamily: String
    var fontSize: CGFloat
    var lineSpacing: CGFloat

    // Appearance
    var appearanceOverride: AppearanceMode  // .system, .light, .dark

    // Defaults
    var defaultFocusMode: Bool
    var defaultTypewriterScroll: Bool
}

C-05: MarkdownParser

struct MarkdownParser {
    func parse(_ source: String) -> Markup
    func parseBlock(_ source: String, range: Range<String.Index>) -> [BlockMarkup]
}

C-06: EditorNodeModel

class EditorNodeModel {
    var blocks: [EditorNode]

    // Sync with parser
    func rebuild(from markup: Markup)
    func updateBlocks(editedRange: NSRange, newSource: String, parser: MarkdownParser)

    // Active block tracking
    func blockContaining(position: Int) -> EditorNode?
    func setActiveBlock(_ blockID: EditorNode.ID?)
}

struct EditorNode: Identifiable {
    let id: UUID
    var type: BlockType             // .paragraph, .heading(level:), .codeBlock(lang:), .list, .table, .blockquote, .horizontalRule, .taskList
    var sourceRange: Range<String.Index>
    var inlineRuns: [InlineRun]     // bold, italic, link, code, image, strikethrough
    var isActive: Bool              // cursor is in this block
}

struct InlineRun {
    var type: InlineType            // .bold, .italic, .link(url:), .code, .image(src:,alt:), .strikethrough, .text
    var range: Range<String.Index>
}

C-07: MarkdownRenderer

struct MarkdownRenderer {
    var settings: EditorSettings

    func render(block: EditorNode, appearance: NSAppearance) -> NSAttributedString
    func renderActiveBlock(block: EditorNode, appearance: NSAppearance) -> NSAttributedString
    func renderFullDocument(model: EditorNodeModel, appearance: NSAppearance) -> NSAttributedString
}

C-08: WYSIWYGTextView

struct WYSIWYGTextView: NSViewRepresentable {
    @Binding var viewModel: EditorViewModel
    var settings: EditorSettings

    func makeNSView(context: Context) -> ShoechooTextView
    func updateNSView(_ nsView: ShoechooTextView, context: Context)
}

class ShoechooTextView: NSTextView {
    // Focus mode
    func applyFocusModeDimming(activeBlockRange: NSRange)
    func removeFocusModeDimming()

    // Typewriter scroll
    func scrollToCenterLine(_ lineRange: NSRange)

    // Image drag & drop
    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
}

C-11: ExportService

actor ExportService {
    func generateHTML(from source: String, title: String) async throws -> String
    func generatePDF(from html: String) async throws -> Data
}

C-12: ImageService

actor ImageService {
    func importDroppedImage(_ image: NSImage, to assetsDir: URL) async throws -> String
    func importPastedImage(from pasteboard: NSPasteboard, to assetsDir: URL) async throws -> String
    func generateFilename(for image: NSImage) -> String
}

C-13: FileService

actor FileService {
    func createDirectoryIfNeeded(at url: URL) async throws
    func fileExists(at url: URL) -> Bool
    func safeWrite(_ data: Data, to url: URL) async throws
}