| 1 | import Foundation |
| 2 | |
| 3 | /// Linear scanning avoids regex backtracking on malformed clipboard HTML such |
| 4 | /// as a long run of '<' without a closing '>'. No rendering or I/O occurs. |
| 5 | struct DocsHTMLScanner { |
| 6 | enum Piece { case text(String), tag(String) } |
| 7 | let source: NSString |
| 8 | var offset = 0 |
| 9 | init(_ value: String) { source = value as NSString } |
| 10 | mutating func next() -> Piece? { |
| 11 | guard offset < source.length else { return nil } |
| 12 | let start = offset |
| 13 | if source.character(at: offset) != 60 { |
| 14 | let next = source.range(of: "<", range: NSRange(location: offset, length: source.length - offset)) |
| 15 | offset = next.location == NSNotFound ? source.length : next.location |
| 16 | return .text(source.substring(with: NSRange(location: start, length: offset - start))) |
| 17 | } |
| 18 | if source.length - offset >= 4, source.substring(with: NSRange(location: offset, length: 4)) == "<!--" { |
| 19 | let end = source.range(of: "-->", range: NSRange(location: offset + 4, length: source.length - offset - 4)) |
| 20 | offset = end.location == NSNotFound ? source.length : NSMaxRange(end) |
| 21 | return .tag("<!-- -->") |
| 22 | } |
| 23 | var quote: unichar = 0 |
| 24 | offset += 1 |
| 25 | while offset < source.length { |
| 26 | let value = source.character(at: offset) |
| 27 | offset += 1 |
| 28 | if quote != 0 { |
| 29 | if value == quote { quote = 0 } |
| 30 | } else if value == 34 || value == 39 { quote = value } |
| 31 | else if value == 62 { return .tag(source.substring(with: NSRange(location: start, length: offset - start))) } |
| 32 | } |
| 33 | return .text(source.substring(from: start)) |
| 34 | } |
| 35 | static func attributes(_ value: String) -> [String: String] { |
| 36 | let source = value as NSString |
| 37 | var index = 0 |
| 38 | var result: [String: String] = [:] |
| 39 | let accepted: Set<String> = ["href", "title", "style", "class", "src", "alt", "start", "align", "face"] |
| 40 | func space(_ value: unichar) -> Bool { [9, 10, 12, 13, 32].contains(value) } |
| 41 | while index < source.length { |
| 42 | while index < source.length && space(source.character(at: index)) { index += 1 } |
| 43 | let start = index |
| 44 | while index < source.length && !space(source.character(at: index)) && source.character(at: index) != 61 { index += 1 } |
| 45 | let key = index - start <= 128 ? source.substring(with: NSRange(location: start, length: index - start)).lowercased() : "" |
| 46 | while index < source.length && space(source.character(at: index)) { index += 1 } |
| 47 | guard index < source.length else { break } |
| 48 | guard source.character(at: index) == 61 else { continue } |
| 49 | index += 1 |
| 50 | while index < source.length && space(source.character(at: index)) { index += 1 } |
| 51 | guard index < source.length else { break } |
| 52 | let first = source.character(at: index) |
| 53 | let quoted = first == 34 || first == 39 |
| 54 | if quoted { index += 1 } |
| 55 | let content = index |
| 56 | while index < source.length && (quoted ? source.character(at: index) != first : !space(source.character(at: index))) { index += 1 } |
| 57 | if accepted.contains(key) { result[key] = source.substring(with: NSRange(location: content, length: index - content)) } |
| 58 | if quoted && index < source.length { index += 1 } |
| 59 | } |
| 60 | return result |
| 61 | } |
| 62 | } |
| 63 | |