Skip to content

Implement attributes enumerating in NSAttributedString #726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 87 additions & 17 deletions Foundation/NSAttributedString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,78 @@ open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCo

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

open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([String : Any], NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) { NSUnimplemented() }
open func enumerateAttribute(_ attrName: String, in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) { NSUnimplemented() }
open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([String : Any], NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
_enumerate(in: enumerationRange, reversed: opts.contains(.reverse)) { currentIndex, stop in
var attributesEffectiveRange = NSRange(location: NSNotFound, length: 0)
let attributesInRange: [String : Any]
if opts.contains(.longestEffectiveRangeNotRequired) {
attributesInRange = attributes(at: currentIndex, effectiveRange: &attributesEffectiveRange)
} else {
attributesInRange = attributes(at: currentIndex, longestEffectiveRange: &attributesEffectiveRange, in: enumerationRange)
}

var shouldStop = false
block(attributesInRange, attributesEffectiveRange, &shouldStop)
stop.pointee = shouldStop

return attributesEffectiveRange
}
}

open func enumerateAttribute(_ attrName: String, in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
_enumerate(in: enumerationRange, reversed: opts.contains(.reverse)) { currentIndex, stop in
var attributeEffectiveRange = NSRange(location: NSNotFound, length: 0)
let attributeInRange: Any?
if opts.contains(.longestEffectiveRangeNotRequired) {
attributeInRange = attribute(attrName, at: currentIndex, effectiveRange: &attributeEffectiveRange)
} else {
attributeInRange = attribute(attrName, at: currentIndex, longestEffectiveRange: &attributeEffectiveRange, in: enumerationRange)
}

var shouldStop = false
block(attributeInRange, attributeEffectiveRange, &shouldStop)
stop.pointee = shouldStop

return attributeEffectiveRange
}
}

}

private extension NSAttributedString {

struct AttributeEnumerationRange {
let startIndex: Int
let endIndex: Int
let reversed: Bool
var currentIndex: Int

var hasMore: Bool {
if reversed {
return currentIndex >= endIndex
} else {
return currentIndex <= endIndex
}
}

init(range: NSRange, reversed: Bool) {
let lowerBound = range.location
let upperBound = range.location + range.length - 1
self.reversed = reversed
startIndex = reversed ? upperBound : lowerBound
endIndex = reversed ? lowerBound : upperBound
currentIndex = startIndex
}

mutating func advance(step: Int = 1) {
if reversed {
currentIndex -= step
} else {
currentIndex += step
}
}
}

struct RangeInfo {
let rangePointer: NSRangePointer?
let shouldFetchLongestEffectiveRange: Bool
Expand Down Expand Up @@ -138,11 +204,9 @@ private extension NSAttributedString {
results[stringKey] = value
}

// Update effective range
let hasAttrs = results.count > 0
rangeInfo.rangePointer?.pointee.location = hasAttrs ? cfRangePointer.pointee.location : NSNotFound
rangeInfo.rangePointer?.pointee.length = hasAttrs ? cfRangePointer.pointee.length : 0

// Update effective range and return the results
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
return results
}
}
Expand All @@ -159,14 +223,20 @@ private extension NSAttributedString {
}

// Update effective range and return the result
if let attribute = attribute {
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
return attribute
} else {
rangeInfo.rangePointer?.pointee.location = NSNotFound
rangeInfo.rangePointer?.pointee.length = 0
return nil
rangeInfo.rangePointer?.pointee.location = cfRangePointer.pointee.location
rangeInfo.rangePointer?.pointee.length = cfRangePointer.pointee.length
return attribute
}
}

func _enumerate(in enumerationRange: NSRange, reversed: Bool, using block: (Int, UnsafeMutablePointer<ObjCBool>) -> NSRange) {
var attributeEnumerationRange = AttributeEnumerationRange(range: enumerationRange, reversed: reversed)
while attributeEnumerationRange.hasMore {
var stop = false
let effectiveRange = block(attributeEnumerationRange.currentIndex, &stop)
attributeEnumerationRange.advance(step: effectiveRange.length)
if stop {
break
}
}
}
Expand Down Expand Up @@ -197,8 +267,8 @@ extension NSAttributedString {
public init(rawValue: UInt) {
self.rawValue = rawValue
}
public static let Reverse = EnumerationOptions(rawValue: 1 << 1)
public static let LongestEffectiveRangeNotRequired = EnumerationOptions(rawValue: 1 << 20)
public static let reverse = EnumerationOptions(rawValue: 1 << 1)
public static let longestEffectiveRangeNotRequired = EnumerationOptions(rawValue: 1 << 20)
}

}
Expand Down
147 changes: 139 additions & 8 deletions TestFoundation/TestNSAttributedString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class TestNSAttributedString : XCTestCase {
("test_initWithString", test_initWithString),
("test_initWithStringAndAttributes", test_initWithStringAndAttributes),
("test_longestEffectiveRange", test_longestEffectiveRange),
("test_enumerateAttributeWithName", test_enumerateAttributeWithName),
("test_enumerateAttributes", test_enumerateAttributes),
]
}

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

var range = NSRange()
let attrs = attrString.attributes(at: 0, effectiveRange: &range)
XCTAssertEqual(range.location, NSNotFound)
XCTAssertEqual(range.length, 0)
XCTAssertEqual(range.location, 0)
XCTAssertEqual(range.length, string.utf16.count)
XCTAssertEqual(attrs.count, 0)

let attribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
XCTAssertNil(attribute)
XCTAssertEqual(range.location, NSNotFound)
XCTAssertEqual(range.length, 0)
XCTAssertEqual(range.location, 0)
XCTAssertEqual(range.length, string.utf16.count)
}

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

let invalidAttribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
XCTAssertNil(invalidAttribute)
XCTAssertEqual(range.location, NSNotFound)
XCTAssertEqual(range.length, 0)
XCTAssertEqual(range.location, 0)
XCTAssertEqual(range.length, string.utf16.count)

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

func test_enumerateAttributeWithName() {
let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."

let attrKey1 = "attribute.placeholder.key1"
let attrValue1 = "attribute.placeholder.value1"
let attrRange1 = NSRange(location: 0, length: 20)
let attrRange2 = NSRange(location: 18, length: 10)

let attrKey3 = "attribute.placeholder.key3"
let attrValue3 = "attribute.placeholder.value3"
let attrRange3 = NSRange(location: 40, length: 5)

let attrString = NSMutableAttributedString(string: string)
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange1)
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange2)
attrString.addAttribute(attrKey3, value: attrValue3, range: attrRange3)

let fullRange = NSRange(location: 0, length: attrString.length)

var rangeDescriptionString = ""
var attrDescriptionString = ""
attrString.enumerateAttribute(attrKey1, in: fullRange) { attr, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrDescriptionString.append(self.describe(attr: attr))
}
XCTAssertEqual(rangeDescriptionString, "(0,28)(28,116)")
XCTAssertEqual(attrDescriptionString, "\(attrValue1)|nil|")

rangeDescriptionString = ""
attrDescriptionString = ""
attrString.enumerateAttribute(attrKey1, in: fullRange, options: [.reverse]) { attr, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrDescriptionString.append(self.describe(attr: attr))
}
XCTAssertEqual(rangeDescriptionString, "(28,116)(0,28)")
XCTAssertEqual(attrDescriptionString, "nil|\(attrValue1)|")

rangeDescriptionString = ""
attrDescriptionString = ""
attrString.enumerateAttribute(attrKey1, in: fullRange, options: [.longestEffectiveRangeNotRequired]) { attr, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrDescriptionString.append(self.describe(attr: attr))
}
XCTAssertEqual(rangeDescriptionString, "(0,28)(28,12)(40,5)(45,99)")
XCTAssertEqual(attrDescriptionString, "\(attrValue1)|nil|nil|nil|")
}

func test_enumerateAttributes() {
let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."

let attrKey1 = "attribute.placeholder.key1"
let attrValue1 = "attribute.placeholder.value1"
let attrRange1 = NSRange(location: 0, length: 20)

let attrKey2 = "attribute.placeholder.key2"
let attrValue2 = "attribute.placeholder.value2"
let attrRange2 = NSRange(location: 18, length: 10)

let attrKey3 = "attribute.placeholder.key3"
let attrValue3 = "attribute.placeholder.value3"
let attrRange3 = NSRange(location: 40, length: 5)

let attrString = NSMutableAttributedString(string: string)
attrString.addAttribute(attrKey1, value: attrValue1, range: attrRange1)
attrString.addAttribute(attrKey2, value: attrValue2, range: attrRange2)
attrString.addAttribute(attrKey3, value: attrValue3, range: attrRange3)

let fullRange = NSRange(location: 0, length: attrString.length)

var rangeDescriptionString = ""
var attrsDescriptionString = ""
attrString.enumerateAttributes(in: fullRange) { attrs, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrsDescriptionString.append(self.describe(attrs: attrs))
}
XCTAssertEqual(rangeDescriptionString, "(0,18)(18,2)(20,8)(28,12)(40,5)(45,99)")
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][:]")

rangeDescriptionString = ""
attrsDescriptionString = ""
attrString.enumerateAttributes(in: fullRange, options: [.reverse]) { attrs, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrsDescriptionString.append(self.describe(attrs: attrs))
}
XCTAssertEqual(rangeDescriptionString, "(45,99)(40,5)(28,12)(20,8)(18,2)(0,18)")
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]")

let partialRange = NSRange(location: 0, length: 10)

rangeDescriptionString = ""
attrsDescriptionString = ""
attrString.enumerateAttributes(in: partialRange) { attrs, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrsDescriptionString.append(self.describe(attrs: attrs))
}
XCTAssertEqual(rangeDescriptionString, "(0,10)")
XCTAssertEqual(attrsDescriptionString, "[attribute.placeholder.key1:attribute.placeholder.value1]")

rangeDescriptionString = ""
attrsDescriptionString = ""
attrString.enumerateAttributes(in: partialRange, options: [.reverse]) { attrs, range, stop in
rangeDescriptionString.append(self.describe(range: range))
attrsDescriptionString.append(self.describe(attrs: attrs))
}
XCTAssertEqual(rangeDescriptionString, "(0,10)")
XCTAssertEqual(attrsDescriptionString, "[attribute.placeholder.key1:attribute.placeholder.value1]")
}
}

fileprivate extension TestNSAttributedString {

fileprivate func describe(range: NSRange) -> String {
return "(\(range.location),\(range.length))"
}

fileprivate func describe(attr: Any?) -> String {
if let attr = attr {
return "\(attr)" + "|"
} else {
return "nil" + "|"
}
}

fileprivate func describe(attrs: [String : Any]) -> String {
if attrs.count > 0 {
return "[" + attrs.map({ "\($0):\($1)" }).sorted(by: { $0 < $1 }).joined(separator: ",") + "]"
} else {
return "[:]"
}
}
}

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

}
}