|
8 | 8 |
|
9 | 9 | import UIKit |
10 | 10 |
|
| 11 | +private let scriptedTextSizeRatio: CGFloat = 0.618 |
| 12 | + |
11 | 13 | extension NSAttributedString { |
12 | 14 | /// Calculates and returns the height needed to fit the text into a width-constrained rect. |
13 | 15 | /// |
@@ -37,4 +39,85 @@ extension NSAttributedString { |
37 | 39 |
|
38 | 40 | return copy.boundingRect(with: constraintSize, options: .usesLineFragmentOrigin, context: nil) |
39 | 41 | } |
| 42 | + |
| 43 | + /// Superscripts substrings of structure ^{substring} and subscripts substrings of structure _{substring}. |
| 44 | + /// |
| 45 | + /// - Parameters: |
| 46 | + /// - font: The base font size for the resulting attributed string. |
| 47 | + /// - applyFont: Specify if the font shall be applied to the resulting attributed string. Defaults to `true`. |
| 48 | + /// |
| 49 | + /// - Returns: The resulting attributed string with superscripted and subscripted substrings. |
| 50 | + public func superAndSubscripted(font: UIFont, applyFont: Bool = true) -> NSAttributedString { |
| 51 | + return subscripted(font: font).superscripted(font: font, applyFont: false) |
| 52 | + } |
| 53 | + |
| 54 | + /// Superscripts substrings of structure ^{substring}. |
| 55 | + /// |
| 56 | + /// - Parameters: |
| 57 | + /// - font: The base font size for the resulting attributed string. |
| 58 | + /// - applyFont: Specify if the font shall be applied to the resulting attributed string. Defaults to `true`. |
| 59 | + /// |
| 60 | + /// - Returns: The resulting attributed string with superscripted substrings. |
| 61 | + public func superscripted(font: UIFont, applyFont: Bool = true) -> NSAttributedString { |
| 62 | + return scripted( |
| 63 | + font: font, |
| 64 | + regex: try! NSRegularExpression(pattern: "\\^\\{([^\\}]*)\\}"), // swiftlint:disable:this force_try |
| 65 | + captureBaselineOffset: font.pointSize * (1.0 - scriptedTextSizeRatio), |
| 66 | + applyFont: applyFont |
| 67 | + ) |
| 68 | + } |
| 69 | + |
| 70 | + /// Subscripts substrings of structure _{substring}. |
| 71 | + /// |
| 72 | + /// - Parameters: |
| 73 | + /// - font: The base font size for the resulting attributed string. |
| 74 | + /// - applyFont: Specify if the font shall be applied to the resulting attributed string. Defaults to `true`. |
| 75 | + /// |
| 76 | + /// - Returns: The resulting attributed string with subscripted substrings. |
| 77 | + public func subscripted(font: UIFont, applyFont: Bool = true) -> NSAttributedString { |
| 78 | + return scripted( |
| 79 | + font: font, |
| 80 | + regex: try! NSRegularExpression(pattern: "\\_\\{([^\\}]*)\\}"), // swiftlint:disable:this force_try |
| 81 | + captureBaselineOffset: font.pointSize * -(scriptedTextSizeRatio / 5), |
| 82 | + applyFont: applyFont |
| 83 | + ) |
| 84 | + } |
| 85 | + |
| 86 | + // swiftlint:disable force_cast |
| 87 | + private func scripted(font: UIFont, regex: NSRegularExpression, captureBaselineOffset: CGFloat, applyFont: Bool = true) -> NSAttributedString { |
| 88 | + // apply font to entire string |
| 89 | + let unprocessedString = self.mutableCopy() as! NSMutableAttributedString |
| 90 | + |
| 91 | + if applyFont { |
| 92 | + unprocessedString.addAttribute(.font, value: font, range: NSRange(location: 0, length: unprocessedString.length)) |
| 93 | + } |
| 94 | + |
| 95 | + // start reading in the string part by part |
| 96 | + let attributedString = NSMutableAttributedString() |
| 97 | + |
| 98 | + while let match = regex.firstMatch( |
| 99 | + in: unprocessedString.string, |
| 100 | + options: .reportCompletion, |
| 101 | + range: NSRange(location: 0, length: unprocessedString.length) |
| 102 | + ) { |
| 103 | + // add substring before match |
| 104 | + let substringBeforeMatch = unprocessedString.attributedSubstring(from: NSRange(location: 0, length: match.range.location)) |
| 105 | + attributedString.append(substringBeforeMatch) |
| 106 | + |
| 107 | + // add match with subscripted style |
| 108 | + let capturedSubstring = unprocessedString.attributedSubstring(from: match.range(at: 1)).mutableCopy() as! NSMutableAttributedString |
| 109 | + let captureFullRange = NSRange(location: 0, length: capturedSubstring.length) |
| 110 | + capturedSubstring.addAttribute(.font, value: font.withSize(font.pointSize * scriptedTextSizeRatio), range: captureFullRange) |
| 111 | + capturedSubstring.addAttribute(.baselineOffset, value: captureBaselineOffset, range: captureFullRange) |
| 112 | + attributedString.append(capturedSubstring) |
| 113 | + |
| 114 | + // strip off the processed part |
| 115 | + unprocessedString.deleteCharacters(in: NSRange(location: 0, length: match.range.location + match.range.length)) |
| 116 | + } |
| 117 | + |
| 118 | + // add substring after last match |
| 119 | + attributedString.append(unprocessedString) |
| 120 | + |
| 121 | + return attributedString.copy() as! NSAttributedString |
| 122 | + } // swiftlint:enable force_cast |
40 | 123 | } |
0 commit comments