import Foundation /// Linear scanning avoids regex backtracking on malformed clipboard HTML such /// as a long run of '<' without a closing '>'. No rendering or I/O occurs. struct DocsHTMLScanner { enum Piece { case text(String), tag(String) } let source: NSString var offset = 0 init(_ value: String) { source = value as NSString } mutating func next() -> Piece? { guard offset < source.length else { return nil } let start = offset if source.character(at: offset) != 60 { let next = source.range(of: "<", range: NSRange(location: offset, length: source.length - offset)) offset = next.location == NSNotFound ? source.length : next.location return .text(source.substring(with: NSRange(location: start, length: offset - start))) } if source.length - offset >= 4, source.substring(with: NSRange(location: offset, length: 4)) == "", range: NSRange(location: offset + 4, length: source.length - offset - 4)) offset = end.location == NSNotFound ? source.length : NSMaxRange(end) return .tag("") } var quote: unichar = 0 offset += 1 while offset < source.length { let value = source.character(at: offset) offset += 1 if quote != 0 { if value == quote { quote = 0 } } else if value == 34 || value == 39 { quote = value } else if value == 62 { return .tag(source.substring(with: NSRange(location: start, length: offset - start))) } } return .text(source.substring(from: start)) } static func attributes(_ value: String) -> [String: String] { let source = value as NSString var index = 0 var result: [String: String] = [:] let accepted: Set = ["href", "title", "style", "class", "src", "alt", "start", "align", "face"] func space(_ value: unichar) -> Bool { [9, 10, 12, 13, 32].contains(value) } while index < source.length { while index < source.length && space(source.character(at: index)) { index += 1 } let start = index while index < source.length && !space(source.character(at: index)) && source.character(at: index) != 61 { index += 1 } let key = index - start <= 128 ? source.substring(with: NSRange(location: start, length: index - start)).lowercased() : "" while index < source.length && space(source.character(at: index)) { index += 1 } guard index < source.length else { break } guard source.character(at: index) == 61 else { continue } index += 1 while index < source.length && space(source.character(at: index)) { index += 1 } guard index < source.length else { break } let first = source.character(at: index) let quoted = first == 34 || first == 39 if quoted { index += 1 } let content = index while index < source.length && (quoted ? source.character(at: index) != first : !space(source.character(at: index))) { index += 1 } if accepted.contains(key) { result[key] = source.substring(with: NSRange(location: content, length: index - content)) } if quoted && index < source.length { index += 1 } } return result } }