// // String+AttributedFormat.swift // BabolatPulse // // Created by Samuel Gallet on 06/09/16. // // import Foundation extension String { /** Create an NSAttributedString using self as format, and apply same attributes for each argument - parameter arguments: Arguments that match format (self) - parameter defaultAttributes: Attributes to apply to whole string by default - parameter formatAttributes: Attributes to apply for each argument - returns: NSAttributedString with same attributes for each argument */ public func attributedString(arguments: [String], defaultAttributes: [NSAttributedString.Key: Any], formatAttributes: [NSAttributedString.Key: Any]) -> NSAttributedString? { return attributedString( arguments: arguments, defaultAttributes: defaultAttributes, differentFormatAttributes: arguments.map { _ in return formatAttributes } ) } /** Create an NSAttributedString using self as format, and apply different attributes for each argument `differentFormatAttributes[i]` is applied to `arguments[i]` - parameter arguments: Arguments that match format (self) - parameter defaultAttributes: Attributes to apply to whole string by default - parameter differentFormatAttributes: Attributes to apply for each argument - returns: NSAttributedString with different attributes for each argument */ public func attributedString(arguments: [String], defaultAttributes: [NSAttributedString.Key: Any], differentFormatAttributes: [[NSAttributedString.Key: Any]]) -> NSAttributedString? { guard arguments.count == differentFormatAttributes.count else { return nil } do { let attributedString = NSMutableAttributedString( string: self, attributes: defaultAttributes ) var locationOffset = 0 try patternMatches().forEach({ (match) in let matchIndex = self.parameterIndex(for: match) let argument = arguments[matchIndex] let format = differentFormatAttributes[matchIndex] let attributedArgument = NSMutableAttributedString( string: argument, attributes: format ) let matchRange = match.range attributedString.replaceCharacters( in: NSRange(location: matchRange.location + locationOffset, length: matchRange.length), with: attributedArgument ) locationOffset += attributedArgument.length - matchRange.length }) return NSAttributedString(attributedString: attributedString) } catch { return nil } } // MARK: - Private private func patternRegularExpression() throws -> NSRegularExpression { return try NSRegularExpression(pattern: "%(([1-9][0-9]*)\\$)?@", options: .caseInsensitive) } private func patternMatches() throws -> [NSTextCheckingResult] { let patternMatches = try patternRegularExpression().matches( in: self, options: [], range: NSRange(location: 0, length: (self as NSString).length) ) return patternMatches } private func parameterIndex(for patternMatch: NSTextCheckingResult) -> Int { let matchRange = patternMatch.range(at: 2) guard matchRange.location != NSNotFound && matchRange.length > 0 else { return 0 } let parameterString = (self as NSString).substring(with: matchRange) guard let parameterIndex = Int(parameterString) else { return 0 } return parameterIndex - 1 } } @available(iOS 15, tvOS 15.0, *) extension String { /** Create an AttributedString using self as format, and apply same attributes for each argument - parameter arguments: Arguments that match format (self) - parameter defaultAttributes: Attributes to apply to whole string by default - parameter formatAttributes: Attributes to apply for each argument - returns: AttributedString with same attributes for each argument */ public func attributedString(arguments: [String], defaultAttributes: AttributeContainer, formatAttributes: AttributeContainer) -> AttributedString { return attributedString( arguments: arguments, defaultAttributes: defaultAttributes, differentFormatAttributes: arguments.map { _ in return formatAttributes } ) } /** Create an AttributedString using self as format, and apply different attributes for each argument `differentFormatAttributes[i]` is applied to `arguments[i]` - parameter arguments: Arguments that match format (self) - parameter defaultAttributes: Attributes to apply to whole string by default - parameter differentFormatAttributes: Attributes to apply for each argument - returns: AttributedString with different attributes for each argument */ public func attributedString( arguments: [String], defaultAttributes: AttributeContainer, differentFormatAttributes: [AttributeContainer] ) -> AttributedString { guard arguments.count == differentFormatAttributes.count else { assertionFailure( "Invalid number of format attributes given, got \(arguments.count) argument " + "and \(differentFormatAttributes) format arguments" ) return AttributedString() } let attributedArguments = zip(arguments, differentFormatAttributes).map { AttributedString($0.0, attributes: $0.1) } return attributedString( arguments: attributedArguments, defaultAttributes: defaultAttributes ) } /** Create an AttributedString using self as format - parameter arguments: Arguments that match format (self) - parameter defaultAttributes: Attributes to apply to whole string by default - returns: AttributedString with different attributes for each argument */ public func attributedString( arguments: [AttributedString], defaultAttributes: AttributeContainer ) -> AttributedString { if #available(iOS 16.0, tvOS 16.0, *) { return attributedStringUsingRegex( arguments: arguments, defaultAttributes: defaultAttributes ) } else { return attributedStringUsingNSRegularExpression( arguments: arguments, defaultAttributes: defaultAttributes ) } } } @available(iOS 15, tvOS 15.0, *) extension String { // MARK: - Internal internal func attributedStringUsingNSRegularExpression( arguments: [AttributedString], defaultAttributes: AttributeContainer ) -> AttributedString { do { let nsString = self as NSString var attributedString = AttributedString() var lastIndex = 0 var previousImplicitParameterIndex: Int? try patternMatches().forEach { match in attributedString.append( AttributedString( nsString.substring(with: NSRange(lastIndex.. Int { let matchRange = patternMatch.range(at: 2) guard matchRange.location != NSNotFound && matchRange.length > 0, let parameterString = .some((self as NSString).substring(with: matchRange)), let parameterIndex = Int(parameterString) else { let parameterIndex = previousImplicitParameterIndex.map { $0 + 1 } ?? 0 previousImplicitParameterIndex = parameterIndex return parameterIndex } return parameterIndex - 1 } } @available(iOS 16.0, tvOS 16.0, *) extension String { // MARK: - Internal @available(iOS 16.0, tvOS 16.0, *) internal func attributedStringUsingRegex( arguments: [AttributedString], defaultAttributes: AttributeContainer ) -> AttributedString { var attributedString = AttributedString() var lastIndex = self.startIndex var previousImplicitParameterIndex: Int? // swiftlint:disable:next operator_usage_whitespace self.matches(of: #/%(?:([1-9][0-9]*)\$)?@/#).forEach { (match) in attributedString.append( AttributedString( self[lastIndex.. Int { guard let stringNumber = patternMatch.1, let explicitIndex = Int(stringNumber) else { let parameterIndex = previousImplicitParameterIndex.map { $0 + 1 } ?? 0 previousImplicitParameterIndex = parameterIndex return parameterIndex } return explicitIndex - 1 } }