Skip to content

Commit 5f86566

Browse files
authored
Merge pull request #726 from eyeplum/attributedstring
2 parents e475d7f + 4a18508 commit 5f86566

File tree

2 files changed

+226
-25
lines changed

2 files changed

+226
-25
lines changed

Foundation/NSAttributedString.swift

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,78 @@ open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCo
105105

106106
public init(NSAttributedString attrStr: NSAttributedString) { NSUnimplemented() }
107107

108-
open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([String : Any], NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) { NSUnimplemented() }
109-
open func enumerateAttribute(_ attrName: String, in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) { NSUnimplemented() }
108+
open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([String : Any], NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
109+
_enumerate(in: enumerationRange, reversed: opts.contains(.reverse)) { currentIndex, stop in
110+
var attributesEffectiveRange = NSRange(location: NSNotFound, length: 0)
111+
let attributesInRange: [String : Any]
112+
if opts.contains(.longestEffectiveRangeNotRequired) {
113+
attributesInRange = attributes(at: currentIndex, effectiveRange: &attributesEffectiveRange)
114+
} else {
115+
attributesInRange = attributes(at: currentIndex, longestEffectiveRange: &attributesEffectiveRange, in: enumerationRange)
116+
}
117+
118+
var shouldStop = false
119+
block(attributesInRange, attributesEffectiveRange, &shouldStop)
120+
stop.pointee = shouldStop
121+
122+
return attributesEffectiveRange
123+
}
124+
}
110125

126+
open func enumerateAttribute(_ attrName: String, in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
127+
_enumerate(in: enumerationRange, reversed: opts.contains(.reverse)) { currentIndex, stop in
128+
var attributeEffectiveRange = NSRange(location: NSNotFound, length: 0)
129+
let attributeInRange: Any?
130+
if opts.contains(.longestEffectiveRangeNotRequired) {
131+
attributeInRange = attribute(attrName, at: currentIndex, effectiveRange: &attributeEffectiveRange)
132+
} else {
133+
attributeInRange = attribute(attrName, at: currentIndex, longestEffectiveRange: &attributeEffectiveRange, in: enumerationRange)
134+
}
135+
136+
var shouldStop = false
137+
block(attributeInRange, attributeEffectiveRange, &shouldStop)
138+
stop.pointee = shouldStop
139+
140+
return attributeEffectiveRange
141+
}
142+
}
143+
111144
}
112145

113146
private extension NSAttributedString {
147+
148+
struct AttributeEnumerationRange {
149+
let startIndex: Int
150+
let endIndex: Int
151+
let reversed: Bool
152+
var currentIndex: Int
153+
154+
var hasMore: Bool {
155+
if reversed {
156+
return currentIndex >= endIndex
157+
} else {
158+
return currentIndex <= endIndex
159+
}
160+
}
161+
162+
init(range: NSRange, reversed: Bool) {
163+
let lowerBound = range.location
164+
let upperBound = range.location + range.length - 1
165+
self.reversed = reversed
166+
startIndex = reversed ? upperBound : lowerBound
167+
endIndex = reversed ? lowerBound : upperBound
168+
currentIndex = startIndex
169+
}
170+
171+
mutating func advance(step: Int = 1) {
172+
if reversed {
173+
currentIndex -= step
174+
} else {
175+
currentIndex += step
176+
}
177+
}
178+
}
179+
114180
struct RangeInfo {
115181
let rangePointer: NSRangePointer?
116182
let shouldFetchLongestEffectiveRange: Bool
@@ -138,11 +204,9 @@ private extension NSAttributedString {
138204
results[stringKey] = value
139205
}
140206

141-
// Update effective range
142-
let hasAttrs = results.count > 0
143-
rangeInfo.rangePointer?.pointee.location = hasAttrs ? cfRangePointer.pointee.location : NSNotFound
144-
rangeInfo.rangePointer?.pointee.length = hasAttrs ? cfRangePointer.pointee.length : 0
145-
207+
// Update effective range and return the results
208+
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
209+
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
146210
return results
147211
}
148212
}
@@ -159,14 +223,20 @@ private extension NSAttributedString {
159223
}
160224

161225
// Update effective range and return the result
162-
if let attribute = attribute {
163-
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
164-
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
165-
return attribute
166-
} else {
167-
rangeInfo.rangePointer?.pointee.location = NSNotFound
168-
rangeInfo.rangePointer?.pointee.length = 0
169-
return nil
226+
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
227+
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
228+
return attribute
229+
}
230+
}
231+
232+
func _enumerate(in enumerationRange: NSRange, reversed: Bool, using block: (Int, UnsafeMutablePointer<ObjCBool>) -> NSRange) {
233+
var attributeEnumerationRange = AttributeEnumerationRange(range: enumerationRange, reversed: reversed)
234+
while attributeEnumerationRange.hasMore {
235+
var stop = false
236+
let effectiveRange = block(attributeEnumerationRange.currentIndex, &stop)
237+
attributeEnumerationRange.advance(step: effectiveRange.length)
238+
if stop {
239+
break
170240
}
171241
}
172242
}
@@ -197,8 +267,8 @@ extension NSAttributedString {
197267
public init(rawValue: UInt) {
198268
self.rawValue = rawValue
199269
}
200-
public static let Reverse = EnumerationOptions(rawValue: 1 << 1)
201-
public static let LongestEffectiveRangeNotRequired = EnumerationOptions(rawValue: 1 << 20)
270+
public static let reverse = EnumerationOptions(rawValue: 1 << 1)
271+
public static let longestEffectiveRangeNotRequired = EnumerationOptions(rawValue: 1 << 20)
202272
}
203273

204274
}

TestFoundation/TestNSAttributedString.swift

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class TestNSAttributedString : XCTestCase {
2626
("test_initWithString", test_initWithString),
2727
("test_initWithStringAndAttributes", test_initWithStringAndAttributes),
2828
("test_longestEffectiveRange", test_longestEffectiveRange),
29+
("test_enumerateAttributeWithName", test_enumerateAttributeWithName),
30+
("test_enumerateAttributes", test_enumerateAttributes),
2931
]
3032
}
3133

@@ -37,14 +39,14 @@ class TestNSAttributedString : XCTestCase {
3739

3840
var range = NSRange()
3941
let attrs = attrString.attributes(at: 0, effectiveRange: &range)
40-
XCTAssertEqual(range.location, NSNotFound)
41-
XCTAssertEqual(range.length, 0)
42+
XCTAssertEqual(range.location, 0)
43+
XCTAssertEqual(range.length, string.utf16.count)
4244
XCTAssertEqual(attrs.count, 0)
4345

4446
let attribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
4547
XCTAssertNil(attribute)
46-
XCTAssertEqual(range.location, NSNotFound)
47-
XCTAssertEqual(range.length, 0)
48+
XCTAssertEqual(range.location, 0)
49+
XCTAssertEqual(range.length, string.utf16.count)
4850
}
4951

5052
func test_initWithStringAndAttributes() {
@@ -67,8 +69,8 @@ class TestNSAttributedString : XCTestCase {
6769

6870
let invalidAttribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
6971
XCTAssertNil(invalidAttribute)
70-
XCTAssertEqual(range.location, NSNotFound)
71-
XCTAssertEqual(range.length, 0)
72+
XCTAssertEqual(range.location, 0)
73+
XCTAssertEqual(range.length, string.utf16.count)
7274

7375
let attribute = attrString.attribute("attribute.placeholder.key", at: 0, effectiveRange: &range)
7476
XCTAssertEqual(range.location, 0)
@@ -105,6 +107,136 @@ class TestNSAttributedString : XCTestCase {
105107
XCTAssertEqual(range.length, 28)
106108
}
107109

110+
func test_enumerateAttributeWithName() {
111+
let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."
112+
113+
let attrKey1 = "attribute.placeholder.key1"
114+
let attrValue1 = "attribute.placeholder.value1"
115+
let attrRange1 = NSRange(location: 0, length: 20)
116+
let attrRange2 = NSRange(location: 18, length: 10)
117+
118+
let attrKey3 = "attribute.placeholder.key3"
119+
let attrValue3 = "attribute.placeholder.value3"
120+
let attrRange3 = NSRange(location: 40, length: 5)
121+
122+
let attrString = NSMutableAttributedString(string: string)
123+
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange1)
124+
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange2)
125+
attrString.addAttribute(attrKey3, value: attrValue3, range: attrRange3)
126+
127+
let fullRange = NSRange(location: 0, length: attrString.length)
128+
129+
var rangeDescriptionString = ""
130+
var attrDescriptionString = ""
131+
attrString.enumerateAttribute(attrKey1, in: fullRange) { attr, range, stop in
132+
rangeDescriptionString.append(self.describe(range: range))
133+
attrDescriptionString.append(self.describe(attr: attr))
134+
}
135+
XCTAssertEqual(rangeDescriptionString, "(0,28)(28,116)")
136+
XCTAssertEqual(attrDescriptionString, "\(attrValue1)|nil|")
137+
138+
rangeDescriptionString = ""
139+
attrDescriptionString = ""
140+
attrString.enumerateAttribute(attrKey1, in: fullRange, options: [.reverse]) { attr, range, stop in
141+
rangeDescriptionString.append(self.describe(range: range))
142+
attrDescriptionString.append(self.describe(attr: attr))
143+
}
144+
XCTAssertEqual(rangeDescriptionString, "(28,116)(0,28)")
145+
XCTAssertEqual(attrDescriptionString, "nil|\(attrValue1)|")
146+
147+
rangeDescriptionString = ""
148+
attrDescriptionString = ""
149+
attrString.enumerateAttribute(attrKey1, in: fullRange, options: [.longestEffectiveRangeNotRequired]) { attr, range, stop in
150+
rangeDescriptionString.append(self.describe(range: range))
151+
attrDescriptionString.append(self.describe(attr: attr))
152+
}
153+
XCTAssertEqual(rangeDescriptionString, "(0,28)(28,12)(40,5)(45,99)")
154+
XCTAssertEqual(attrDescriptionString, "\(attrValue1)|nil|nil|nil|")
155+
}
156+
157+
func test_enumerateAttributes() {
158+
let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."
159+
160+
let attrKey1 = "attribute.placeholder.key1"
161+
let attrValue1 = "attribute.placeholder.value1"
162+
let attrRange1 = NSRange(location: 0, length: 20)
163+
164+
let attrKey2 = "attribute.placeholder.key2"
165+
let attrValue2 = "attribute.placeholder.value2"
166+
let attrRange2 = NSRange(location: 18, length: 10)
167+
168+
let attrKey3 = "attribute.placeholder.key3"
169+
let attrValue3 = "attribute.placeholder.value3"
170+
let attrRange3 = NSRange(location: 40, length: 5)
171+
172+
let attrString = NSMutableAttributedString(string: string)
173+
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange1)
174+
attrString.addAttribute(attrKey2, value: attrValue2, range: attrRange2)
175+
attrString.addAttribute(attrKey3, value: attrValue3, range: attrRange3)
176+
177+
let fullRange = NSRange(location: 0, length: attrString.length)
178+
179+
var rangeDescriptionString = ""
180+
var attrsDescriptionString = ""
181+
attrString.enumerateAttributes(in: fullRange) { attrs, range, stop in
182+
rangeDescriptionString.append(self.describe(range: range))
183+
attrsDescriptionString.append(self.describe(attrs: attrs))
184+
}
185+
XCTAssertEqual(rangeDescriptionString, "(0,18)(18,2)(20,8)(28,12)(40,5)(45,99)")
186+
XCTAssertEqual(attrsDescriptionString, "[attribute.placeholder.key1:attribute.placeholder.value1][attribute.placeholder.key1:attribute.placeholder.value1,attribute.placeholder.key2:attribute.placeholder.value2][attribute.placeholder.key2:attribute.placeholder.value2][:][attribute.placeholder.key3:attribute.placeholder.value3][:]")
187+
188+
rangeDescriptionString = ""
189+
attrsDescriptionString = ""
190+
attrString.enumerateAttributes(in: fullRange, options: [.reverse]) { attrs, range, stop in
191+
rangeDescriptionString.append(self.describe(range: range))
192+
attrsDescriptionString.append(self.describe(attrs: attrs))
193+
}
194+
XCTAssertEqual(rangeDescriptionString, "(45,99)(40,5)(28,12)(20,8)(18,2)(0,18)")
195+
XCTAssertEqual(attrsDescriptionString, "[:][attribute.placeholder.key3:attribute.placeholder.value3][:][attribute.placeholder.key2:attribute.placeholder.value2][attribute.placeholder.key1:attribute.placeholder.value1,attribute.placeholder.key2:attribute.placeholder.value2][attribute.placeholder.key1:attribute.placeholder.value1]")
196+
197+
let partialRange = NSRange(location: 0, length: 10)
198+
199+
rangeDescriptionString = ""
200+
attrsDescriptionString = ""
201+
attrString.enumerateAttributes(in: partialRange) { attrs, range, stop in
202+
rangeDescriptionString.append(self.describe(range: range))
203+
attrsDescriptionString.append(self.describe(attrs: attrs))
204+
}
205+
XCTAssertEqual(rangeDescriptionString, "(0,10)")
206+
XCTAssertEqual(attrsDescriptionString, "[attribute.placeholder.key1:attribute.placeholder.value1]")
207+
208+
rangeDescriptionString = ""
209+
attrsDescriptionString = ""
210+
attrString.enumerateAttributes(in: partialRange, options: [.reverse]) { attrs, range, stop in
211+
rangeDescriptionString.append(self.describe(range: range))
212+
attrsDescriptionString.append(self.describe(attrs: attrs))
213+
}
214+
XCTAssertEqual(rangeDescriptionString, "(0,10)")
215+
XCTAssertEqual(attrsDescriptionString, "[attribute.placeholder.key1:attribute.placeholder.value1]")
216+
}
217+
}
218+
219+
fileprivate extension TestNSAttributedString {
220+
221+
fileprivate func describe(range: NSRange) -> String {
222+
return "(\(range.location),\(range.length))"
223+
}
224+
225+
fileprivate func describe(attr: Any?) -> String {
226+
if let attr = attr {
227+
return "\(attr)" + "|"
228+
} else {
229+
return "nil" + "|"
230+
}
231+
}
232+
233+
fileprivate func describe(attrs: [String : Any]) -> String {
234+
if attrs.count > 0 {
235+
return "[" + attrs.map({ "\($0):\($1)" }).sorted(by: { $0 < $1 }).joined(separator: ",") + "]"
236+
} else {
237+
return "[:]"
238+
}
239+
}
108240
}
109241

110242
class TestNSMutableAttributedString : XCTestCase {
@@ -119,6 +251,5 @@ class TestNSMutableAttributedString : XCTestCase {
119251
let string = "Lorem 😀 ipsum dolor sit amet, consectetur adipiscing elit. ⌘ Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit. ಠ_ರೃ"
120252
let mutableAttrString = NSMutableAttributedString(string: string)
121253
XCTAssertEqual(mutableAttrString.mutableString, NSMutableString(string: string))
122-
}
123-
254+
}
124255
}

0 commit comments

Comments
 (0)