// // AttributedString+Trim.swift // Tusker // // Created by Shadowfacts on 8/29/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import Foundation private let ASCII_NEWLINE: unichar = 10 private let ASCII_SPACE: unichar = 32 extension NSAttributedString { var fullRange: NSRange { return NSRange(location: 0, length: self.length) } /// Creates a new string with the whitespace collapsed according to the CSS Text Module Level 3 rules. /// See https://www.w3.org/TR/css-text-3/#white-space-phase-1 func collapsingWhitespace() -> NSAttributedString { let mut = NSMutableAttributedString(attributedString: self) mut.collapseWhitespace() return mut } } extension NSMutableAttributedString { func trimLeadingCharactersInSet(_ charSet: CharacterSet) { var end = string.startIndex while end < string.endIndex && charSet.contains(string.unicodeScalars[end]) { end = string.unicodeScalars.index(after: end) } if end > string.startIndex { let length = string.utf16.distance(from: string.startIndex, to: end) replaceCharacters(in: NSRange(location: 0, length: length), with: "") } } func trimTrailingCharactersInSet(_ charSet: CharacterSet) { if string.isEmpty { return } var start = string.index(before: string.endIndex) while start > string.startIndex && charSet.contains(string.unicodeScalars[start]) { start = string.unicodeScalars.index(before: start) } if start < string.endIndex { if start != string.startIndex || !charSet.contains(string.unicodeScalars[start]) { start = string.unicodeScalars.index(after: start) } let location = string.utf16.distance(from: string.startIndex, to: start) let length = string.utf16.distance(from: start, to: string.endIndex) replaceCharacters(in: NSRange(location: location, length: length), with: "") } } /// Collapses whitespace in this string according to the CSS Text Module Level 3 rules. /// See https://www.w3.org/TR/css-text-3/#white-space-phase-1 func collapseWhitespace() { let str = self.mutableString var i = 0 while i < str.length { if str.character(at: i) == ASCII_NEWLINE { var j: Int if i > 0 { // scan backwards to find beginning of space characters preceeding newline j = i - 1 while j >= 0 { if str.character(at: j) != ASCII_SPACE { break } j -= 1 } // add one after loop completes because start of range is _inclusive_ j += 1 } else { j = 0 } var k: Int if i < str.length - 1 { // scan forwards to find end of space characters following newline k = i + 1 while k < str.length { if str.character(at: k) != ASCII_SPACE { break } k += 1 } // don't need to subtract one before breaking out of loop, because end of range is _exclusive_ } else { // range end is _exclusive_, so use whole string length that way last character is included k = str.length } // if there's only one character to be replaced, that means we'd be replacing the newline with a newline, so don't bother if k - j > 1 { str.replaceCharacters(in: NSRange(location: j, length: k - j), with: "\n") // continue scanning through the string starting after the newline we just inserted i = j } } i += 1 } } }