Skip to content

Commit

Permalink
Merge pull request swiftlang#726 from eyeplum/attributedstring
Browse files Browse the repository at this point in the history
  • Loading branch information
swift-ci authored Nov 30, 2016
2 parents e475d7f + 4a18508 commit 5f86566
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 25 deletions.
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))
}

}
}

0 comments on commit 5f86566

Please sign in to comment.