import Foundation #if os(macOS) import AppKit #else import UIKit #endif @MainActor extension DocsTextView { private var ruleContent: NSAttributedString { #if os(macOS) attributedString() #else attributedText #endif } var ruleCaret: Int { ruleSelection.location } private var ruleSelection: NSRange { #if os(macOS) selectedRange() #else selectedRange #endif } private func replaceRule(_ range: NSRange, with value: NSAttributedString) { #if os(macOS) textStorage?.replaceCharacters(in: range, with: value) setSelectedRange(NSRange(location: range.location + value.length, length: 0)) didChangeText() #else textStorage.replaceCharacters(in: range, with: value) selectedRange = NSRange(location: range.location + value.length, length: 0) delegate?.textViewDidChange?(self) #endif } func interceptInput(_ text: String, range: NSRange) -> Bool { guard isEditable else { return true } if text.isEmpty, lastInputRule != nil, revertInputRule() { return true } lastInputRule = nil if let plan = DocumentInputRules.plan(in: ruleContent, range: range, replacement: text) { let command: [String: JSONValue] = ["op": .string("display_autoformat"), "at": .number(Double(plan.paragraphLocation)), "length": .number(Double(plan.removeLength)), "kind": .string(plan.kind), "level": .number(Double(plan.level))] guard ruleCommand?(command) == true else { return true } replaceRule(NSRange(location: plan.paragraphLocation, length: plan.removeLength), with: NSAttributedString(string: "")) let style = (plan.priorParagraphStyle.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle() if plan.kind == "bullet_list" || plan.kind == "ordered_list" { style.textLists = [NSTextList(markerFormat: plan.kind == "bullet_list" ? .disc : .decimal, options: 0)] } let paragraph = (ruleContent.string as NSString).paragraphRange(for: NSRange(location: min(plan.paragraphLocation, max(0, ruleContent.length - 1)), length: 0)) let attributes: [NSAttributedString.Key: Any] = [.paragraphStyle: style, .docsBlockType: plan.kind == "heading" ? "heading" : "paragraph", .docsHeading: plan.level] #if os(macOS) textStorage?.addAttributes(attributes, range: paragraph) #else textStorage.addAttributes(attributes, range: paragraph) #endif lastInputRule = plan return true } if text == "\n", range.length == 0, ruleContent.length > 0 { let raw = ruleContent.string as NSString let paragraph = raw.paragraphRange(for: NSRange(location: min(range.location, raw.length - 1), length: 0)) let value = raw.substring(with: paragraph) let style = ruleContent.attribute(.paragraphStyle, at: min(range.location, raw.length - 1), effectiveRange: nil) as? NSParagraphStyle if value == "\n", style?.textLists.isEmpty == false { guard ruleCommand?(["op": .string("display_exit_list"), "at": .number(Double(range.location)), "kind": .string("paragraph")]) == true else { return true } let plain = (style?.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle() plain.textLists = []; plain.headIndent = 0; plain.firstLineHeadIndent = 0 replaceRule(paragraph, with: NSAttributedString(string: "\n", attributes: [.paragraphStyle: plain])) #if os(macOS) setSelectedRange(range) #else selectedRange = range #endif return true } } return false } func revertInputRule() -> Bool { guard !isComposing, isEditable, let plan = lastInputRule, ruleSelection == NSRange(location: plan.paragraphLocation, length: 0) else { lastInputRule = nil; return false } let command: [String: JSONValue] = ["op": .string("display_revert_rule"), "at": .number(Double(plan.paragraphLocation)), "kind": .string(plan.kind), "prior_type": .string(plan.priorBlockType), "prior_attrs": .object(plan.priorAttrs.mapValues(JSONValue.string)), "text": .string(plan.prefix)] guard ruleCommand?(command) == true else { return true } replaceRule(NSRange(location: plan.paragraphLocation, length: 0), with: NSAttributedString(string: plan.prefix, attributes: [.paragraphStyle: plan.priorParagraphStyle, .docsBlockType: plan.priorBlockType])) let paragraph = (ruleContent.string as NSString).paragraphRange(for: NSRange(location: plan.paragraphLocation, length: 0)) let restored: [NSAttributedString.Key: Any] = [.paragraphStyle: plan.priorParagraphStyle, .docsBlockType: plan.priorBlockType, .docsBlockAttributes: (try? JSONEncoder().encode(plan.priorAttrs)) ?? Data()] #if os(macOS) textStorage?.addAttributes(restored, range: paragraph) #else textStorage.addAttributes(restored, range: paragraph) #endif lastInputRule = nil return true } }