import AppKit final class DocumentNode: NSObject, SourceListItem, Identifiable { let id: UUID @objc dynamic var title: String @objc dynamic var displaysSectionHeading: Bool @objc dynamic var content: String @objc dynamic var children: [Any] required init(id: UUID, title: String = "", displaysSectionHeading: Bool = false, content: String = "", children: [DocumentNode] = []) { self.id = id self.title = title self.displaysSectionHeading = displaysSectionHeading self.content = content self.children = children } convenience init(id: UUID) { self.init(id: id, title: "", children: []) } @objc dynamic var leaf: Bool { children.isEmpty } @objc class func keyPathsForValuesAffectingLeaf() -> Set { [#keyPath(children)] } @objc dynamic var isGroupItem: Bool { false } @objc dynamic var asDocumentNode: Self? { self } @objc dynamic var imageValue: NSImage! { let systemSymbolName = leaf ? "text.page" : "text.book.closed" let configuration = NSImage.SymbolConfiguration(pointSize: NSFont.systemFontSize, weight: .regular) return NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)?.withSymbolConfiguration(configuration) } @objc class func keyPathsForValuesAffectingImageValue() -> Set { return [#keyPath(leaf)] } func update(from string: NSString, blockRanges: [UUID: NSRange]) { if let currentBlockRange = blockRanges[id] { content = "" let displaysSectionHeading = displaysSectionHeading var titleOffset = 0 if displaysSectionHeading { string.enumerateSubstrings(in: currentBlockRange, options: .byParagraphs) { substring, substringRange, enclosingRange, shouldStop in titleOffset = enclosingRange.length self.title = substring ?? "" shouldStop.pointee = true } } var contentRange = currentBlockRange contentRange.location += titleOffset contentRange.length -= titleOffset content = string.substring(with: contentRange) } for index in children.indices { guard let documentNode = children[index] as? DocumentNode else { continue } documentNode.update(from: string, blockRanges: blockRanges) } } func update(from attributedString: NSAttributedString, firstBlockID: UUID) { let string = NSString(string: attributedString.string) let blockRanges = attributedString.blockRanges(firstBlockID: firstBlockID) update(from: string, blockRanges: blockRanges) } } struct DocumentNodeAccumulator { var seen: Set = [] var items: [DocumentNode] = [] mutating func visit(_ item: DocumentNode) { guard !seen.contains(item.id) else { return } seen.insert(item.id) items.append(item) for child in item.children { guard let child = child as? DocumentNode else { continue } visit(child) } } }