/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation public extension String { public func startsWith(_ other: String) -> Bool { // rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets. if other.isEmpty { return true } if let range = self.range(of: other, options: NSString.CompareOptions.anchored) { return range.lowerBound == self.startIndex } return false } public func endsWith(_ other: String) -> Bool { // rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets. if other.isEmpty { return true } if let range = self.range(of: other, options: [NSString.CompareOptions.anchored, NSString.CompareOptions.backwards]) { return range.upperBound == self.endIndex } return false } func escape() -> String? { // We can't guaruntee that strings have a valid string encoding, as this is an entry point for tainted data, // we should be very careful about forcefully dereferencing optional types. // https://stackoverflow.com/questions/33558933/why-is-the-return-value-of-string-addingpercentencoding-optional#33558934 let queryItemDividers = CharacterSet(charactersIn: "?=&") let allowedEscapes = CharacterSet.urlQueryAllowed.symmetricDifference(queryItemDividers) return self.addingPercentEncoding(withAllowedCharacters: allowedEscapes) } func unescape() -> String? { return self.removingPercentEncoding } /** Ellipsizes a String only if it's longer than `maxLength` "ABCDEF".ellipsize(4) // "AB…EF" :param: maxLength The maximum length of the String. :returns: A String with `maxLength` characters or less */ func ellipsize(maxLength: Int) -> String { if (maxLength >= 2) && (self.characters.count > maxLength) { let index1 = self.characters.index(self.startIndex, offsetBy: (maxLength + 1) / 2) // `+ 1` has the same effect as an int ceil let index2 = self.characters.index(self.endIndex, offsetBy: maxLength / -2) return self.substring(to: index1) + "…\u{2060}" + self.substring(from: index2) } return self } private var stringWithAdditionalEscaping: String { return self.replacingOccurrences(of: "|", with: "%7C", options: NSString.CompareOptions(), range: nil) } public var asURL: URL? { // Firefox and NSURL disagree about the valid contents of a URL. // Let's escape | for them. // We'd love to use one of the more sophisticated CFURL* or NSString.* functions, but // none seem to be quite suitable. return URL(string: self) ?? URL(string: self.stringWithAdditionalEscaping) } /// Returns a new string made by removing the leading String characters contained /// in a given character set. public func stringByTrimmingLeadingCharactersInSet(_ set: CharacterSet) -> String { var trimmed = self while trimmed.rangeOfCharacter(from: set)?.lowerBound == trimmed.startIndex { trimmed.remove(at: trimmed.startIndex) } return trimmed } /// Adds a newline at the closest space from the middle of a string. /// Example turning "Mark as Read" into "Mark as\n Read" public func stringSplitWithNewline() -> String { let mid = self.characters.count/2 let arr: [Int] = self.characters.indices.flatMap { if self.characters[$0] == " " { return self.distance(from: startIndex, to: $0) } return nil } guard let closest = arr.enumerated().min(by: { abs($0.1 - mid) < abs($1.1 - mid) }) else { return self } var newString = self newString.insert("\n", at: newString.characters.index(newString.characters.startIndex, offsetBy: closest.element)) return newString } }