| 1 | import Foundation |
| 2 | |
| 3 | struct DocumentWordCounts { |
| 4 | let words: Int |
| 5 | let characters: Int |
| 6 | let charactersNoSpaces: Int |
| 7 | let paragraphs: Int |
| 8 | let selectedWords: Int? |
| 9 | |
| 10 | init(projection: CRDTProjection, content: NSAttributedString, selection: NSRange) { |
| 11 | var whole = "" |
| 12 | func visit(_ node: CRDTNode) { |
| 13 | if node.type == "text" { whole += node.plainText } |
| 14 | else { |
| 15 | if !["image", "hard_break"].contains(node.type), !whole.isEmpty { whole += " " } |
| 16 | for child in node.children ?? [] { visit(child) } |
| 17 | } |
| 18 | } |
| 19 | for node in projection.children { visit(node) } |
| 20 | words = Self.countWords(whole) |
| 21 | characters = whole.utf16.count |
| 22 | charactersNoSpaces = whole.unicodeScalars.filter { !CharacterSet.whitespacesAndNewlines.contains($0) }.reduce(0) { $0 + $1.utf16.count } |
| 23 | paragraphs = projection.children.filter { |
| 24 | ["paragraph", "heading", "code_block", "bullet_list", "ordered_list", "blockquote"].contains($0.type) |
| 25 | }.count |
| 26 | let valid = NSIntersectionRange(selection, NSRange(location: 0, length: content.length)) |
| 27 | if valid.length == 0 { selectedWords = nil } |
| 28 | else { |
| 29 | var selected = "" |
| 30 | content.enumerateAttributes(in: valid) { attributes, range, _ in |
| 31 | if attributes[.docsMarks] is Data { |
| 32 | selected += (content.string as NSString).substring(with: range) |
| 33 | } else if attributes[.docsBlock] != nil { |
| 34 | // Only the paragraph terminator separates words. Inline |
| 35 | // images and hard breaks contribute no text in web counts. |
| 36 | for scalar in (content.string as NSString).substring(with: range).unicodeScalars where scalar == "\n" { selected += " " } |
| 37 | } |
| 38 | } |
| 39 | selectedWords = Self.countWords(selected) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | static func countWords(_ text: String) -> Int { |
| 44 | var count = 0 |
| 45 | text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: [.byWords, .substringNotRequired]) { _, _, _, _ in count += 1 } |
| 46 | return count |
| 47 | } |
| 48 | } |
| 49 | |