// // 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 range = (string as NSString).rangeOfCharacter(from: charSet) while range.length != 0 && range.location == 0 { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: charSet) } } func trimTrailingCharactersInSet(_ charSet: CharacterSet) { var range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) while range.length != 0 && range.length + range.location == length { replaceCharacters(in: range, with: "") range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) } } /// 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 } } }