import Foundation #if os(macOS) import AppKit #else import UIKit #endif enum DocumentInputRules { struct Plan { let kind: String let level: Int let start: Int let paragraphLocation: Int let removeLength: Int let prefix: String let priorBlockType: String let priorAttrs: [String: String] let priorParagraphStyle: NSParagraphStyle } /// Matches fixtures/autoformat.mjs against at most ProseMirror's 500-character /// input-rule context. This describes a transform; it never changes storage. static func plan(in attributed: NSAttributedString, range: NSRange, replacement: String) -> Plan? { guard range.length == 0, range.location >= 0, range.location <= attributed.length, replacement.unicodeScalars.count == 1, let scalar = replacement.unicodeScalars.first, CharacterSet.whitespaces.contains(scalar), !CharacterSet.newlines.contains(scalar) else { return nil } let source = attributed.string as NSString var beginning = range.location while beginning > 0 && source.character(at: beginning - 1) != 10 { guard range.location - beginning < 500 else { return nil } beginning -= 1 } let existing = source.substring(with: NSRange(location: beginning, length: range.location - beginning)) guard !existing.contains("\u{2028}"), !existing.contains("\r") else { return nil } let prefix = existing + replacement let attributes = beginning < attributed.length ? attributed.attributes(at: beginning, effectiveRange: nil) : [:] let priorType = attributes[.docsBlockType] as? String ?? "paragraph" guard priorType != "code_block" else { return nil } var kind: String var level = 0 if prefix.range(of: #"^\s*[-+*]\s\z"#, options: .regularExpression) != nil { kind = "bullet_list" } else if prefix.range(of: #"^[0-9]+\.\s\z"#, options: .regularExpression) != nil { kind = "ordered_list" } else if prefix.range(of: #"^\s*>\s\z"#, options: .regularExpression) != nil { kind = "blockquote" } else if prefix.range(of: #"^#{1,3}\s\z"#, options: .regularExpression) != nil { kind = "heading" level = existing.count } else { return nil } let priorAttrs = (attributes[.docsBlockAttributes] as? Data).flatMap { try? JSONDecoder().decode([String: String].self, from: $0) } ?? [:] let style = (attributes[.paragraphStyle] as? NSParagraphStyle)?.copy() as? NSParagraphStyle ?? NSParagraphStyle.default // The web input rule intentionally starts at 1 regardless of typed digits. return Plan(kind: kind, level: level, start: 1, paragraphLocation: beginning, removeLength: range.location - beginning, prefix: prefix, priorBlockType: priorType, priorAttrs: priorAttrs, priorParagraphStyle: style) } }