// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation public extension NSAttributedString { enum FormattedValue { case plain(String) case font(String, UIFont) } convenience init( format: String, _ arguments: FormattedValue... ) { // FIXME: This styling approach breaks with RTL languages and should be replaced as part of the Strings project self.init( attributedString: zip(format.components(separatedBy: "%@"), arguments.appending(.plain(""))) .map { text, value -> (text: String, value: FormattedValue) in (text, value) } .enumerated() .reduce(into: NSMutableAttributedString()) { result, next in result.append(NSAttributedString(string: next.element.text)) switch next.element.value { case .plain(let value): result.append(NSAttributedString(string: value)) case .font(let value, let font): result.append(NSAttributedString(string: value, attributes: [.font: font])) } } ) } static func with(_ attrStrings: [NSAttributedString]) -> NSAttributedString { let mutableString: NSMutableAttributedString = NSMutableAttributedString() for attrString in attrStrings { mutableString.append(attrString) } return mutableString } func appending(_ attrString: NSAttributedString) -> NSAttributedString { let mutableString: NSMutableAttributedString = NSMutableAttributedString(attributedString: self) mutableString.append(attrString) return mutableString } func appending(string: String, attributes: [Key: Any]? = nil) -> NSAttributedString { return appending(NSAttributedString(string: string, attributes: attributes)) } func adding(attributes: [Key: Any], range: NSRange) -> NSAttributedString { let mutableString: NSMutableAttributedString = NSMutableAttributedString(attributedString: self) mutableString.addAttributes(attributes, range: range) return mutableString } // The actual Swift implementation of 'uppercased' is pretty nuts (see // https://github.com/apple/swift/blob/main/stdlib/public/core/String.swift#L901) // this approach is definitely less efficient but is much simpler and less likely to break private enum CharacterCasing { static let map: [UTF16.CodeUnit: String.UTF16View] = [ "a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "k": "K", "l": "L", "m": "M", "n": "N", "o": "O", "p": "P", "q": "Q", "r": "R", "s": "S", "t": "T", "u": "U", "v": "V", "w": "W", "x": "X", "y": "Y", "z": "Z" ] .reduce(into: [:]) { prev, next in prev[next.key.utf16.first ?? UTF16.CodeUnit()] = next.value.utf16 } } func uppercased() -> NSAttributedString { let result = NSMutableAttributedString(attributedString: self) let uppercasedCharacters = result.string.utf16.map { utf16Char in // Try convert the individual utf16 character to it's uppercase variant // or fallback to the original character (CharacterCasing.map[utf16Char]?.first ?? utf16Char) } result.replaceCharacters( in: NSRange(location: 0, length: length), with: String(utf16CodeUnits: uppercasedCharacters, count: length) ) return result } }