Merge pull request #1281 from spevans/pr_nsattributed_string_fixes

diff --git a/Foundation/NSAttributedString.swift b/Foundation/NSAttributedString.swift
index 3877813..de1464b 100644
--- a/Foundation/NSAttributedString.swift
+++ b/Foundation/NSAttributedString.swift
@@ -9,6 +9,22 @@
 
 import CoreFoundation
 
+public struct NSAttributedStringKey : RawRepresentable, Equatable, Hashable {
+    public let rawValue: String
+
+    public init(_ rawValue: String) {
+        self.rawValue = rawValue
+    }
+
+    public init(rawValue: String) {
+        self.rawValue = rawValue
+    }
+
+    public var hashValue: Int {
+        return rawValue.hashValue
+    }
+}
+
 open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCoding {
     
     private let _cfinfo = _CFInfo(typeID: CFAttributedStringGetTypeID())
@@ -42,12 +58,14 @@
     open func mutableCopy(with zone: NSZone? = nil) -> Any {
         NSUnimplemented()
     }
-    
+
+    /// The character contents of the receiver as an NSString object.
     open var string: String {
         return _string._swiftObject
     }
-    
-    open func attributes(at location: Int, effectiveRange range: NSRangePointer) -> [String : Any] {
+
+    /// Returns the attributes for the character at a given index.
+    open func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedStringKey : Any] {
         let rangeInfo = RangeInfo(
             rangePointer: range,
             shouldFetchLongestEffectiveRange: false,
@@ -55,60 +73,72 @@
         return _attributes(at: location, rangeInfo: rangeInfo)
     }
 
+    /// The length of the receiver’s string object.
     open var length: Int {
         return CFAttributedStringGetLength(_cfObject)
     }
-    
-    open func attribute(_ attrName: String, at location: Int, effectiveRange range: NSRangePointer?) -> Any? {
+
+    /// Returns the value for an attribute with a given name of the character at a given index, and by reference the range over which the attribute applies.
+    open func attribute(_ attrName: NSAttributedStringKey, at location: Int, effectiveRange range: NSRangePointer?) -> Any? {
         let rangeInfo = RangeInfo(
             rangePointer: range,
             shouldFetchLongestEffectiveRange: false,
             longestEffectiveRangeSearchRange: nil)
         return _attribute(attrName, atIndex: location, rangeInfo: rangeInfo)
     }
-    
+
+    /// Returns an NSAttributedString object consisting of the characters and attributes within a given range in the receiver.
     open func attributedSubstring(from range: NSRange) -> NSAttributedString { NSUnimplemented() }
-    
-    open func attributes(at location: Int, longestEffectiveRange range: NSRangePointer?, in rangeLimit: NSRange) -> [String : Any] {
+
+    /// Returns the attributes for the character at a given index, and by reference the range over which the attributes apply.
+    open func attributes(at location: Int, longestEffectiveRange range: NSRangePointer?, in rangeLimit: NSRange) -> [NSAttributedStringKey : Any] {
         let rangeInfo = RangeInfo(
             rangePointer: range,
             shouldFetchLongestEffectiveRange: true,
             longestEffectiveRangeSearchRange: rangeLimit)
         return _attributes(at: location, rangeInfo: rangeInfo)
     }
-    
-    open func attribute(_ attrName: String, at location: Int, longestEffectiveRange range: NSRangePointer?, in rangeLimit: NSRange) -> Any? {
+
+    /// Returns the value for the attribute with a given name of the character at a given index, and by reference the range over which the attribute applies.
+    open func attribute(_ attrName: NSAttributedStringKey, at location: Int, longestEffectiveRange range: NSRangePointer?, in rangeLimit: NSRange) -> Any? {
         let rangeInfo = RangeInfo(
             rangePointer: range,
             shouldFetchLongestEffectiveRange: true,
             longestEffectiveRangeSearchRange: rangeLimit)
         return _attribute(attrName, atIndex: location, rangeInfo: rangeInfo)
     }
-    
+
+    /// Returns a Boolean value that indicates whether the receiver is equal to another given attributed string.
     open func isEqual(to other: NSAttributedString) -> Bool { NSUnimplemented() }
-    
-    public init(string str: String) {
-        _string = str._nsObject
+
+    /// Returns an NSAttributedString object initialized with the characters of a given string and no attribute information.
+    public init(string: String) {
+        _string = string._nsObject
         _attributeArray = CFRunArrayCreate(kCFAllocatorDefault)
         
         super.init()
         addAttributesToAttributeArray(attrs: nil)
     }
-    
-    public init(string str: String, attributes attrs: [String : Any]?) {
-        _string = str._nsObject
+
+    /// Returns an NSAttributedString object initialized with a given string and attributes.
+    public init(string: String, attributes attrs: [NSAttributedStringKey : Any]? = nil) {
+        _string = string._nsObject
         _attributeArray = CFRunArrayCreate(kCFAllocatorDefault)
-        
+
         super.init()
         addAttributesToAttributeArray(attrs: attrs)
     }
-    
-    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) {
+    /// Returns an NSAttributedString object initialized with the characters and attributes of another given attributed string.
+    public init(attributedString: NSAttributedString) {
+        NSUnimplemented()
+    }
+
+    /// Executes the block for each attribute in the range.
+    open func enumerateAttributes(in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: ([NSAttributedStringKey : 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]
+            let attributesInRange: [NSAttributedStringKey : Any]
             if opts.contains(.longestEffectiveRangeNotRequired) {
                 attributesInRange = attributes(at: currentIndex, effectiveRange: &attributesEffectiveRange)
             } else {
@@ -122,8 +152,9 @@
             return attributesEffectiveRange
         }
     }
-    
-    open func enumerateAttribute(_ attrName: String, in enumerationRange: NSRange, options opts: NSAttributedString.EnumerationOptions = [], using block: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Swift.Void) {
+
+    /// Executes the block for the specified attribute run in the specified range.
+    open func enumerateAttribute(_ attrName: NSAttributedStringKey, 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?
@@ -183,9 +214,9 @@
         let longestEffectiveRangeSearchRange: NSRange?
     }
     
-    func _attributes(at location: Int, rangeInfo: RangeInfo) -> [String : Any] {
+    func _attributes(at location: Int, rangeInfo: RangeInfo) -> [NSAttributedStringKey : Any] {
         var cfRange = CFRange()
-        return withUnsafeMutablePointer(to: &cfRange) { (cfRangePointer: UnsafeMutablePointer<CFRange>) -> [String : Any] in
+        return withUnsafeMutablePointer(to: &cfRange) { (cfRangePointer: UnsafeMutablePointer<CFRange>) -> [NSAttributedStringKey : Any] in
             // Get attributes value using CoreFoundation function
             let value: CFDictionary
             if rangeInfo.shouldFetchLongestEffectiveRange, let searchRange = rangeInfo.longestEffectiveRangeSearchRange {
@@ -196,12 +227,12 @@
             
             // Convert the value to [String : AnyObject]
             let dictionary = unsafeBitCast(value, to: NSDictionary.self)
-            var results = [String : Any]()
+            var results = [NSAttributedStringKey : Any]()
             for (key, value) in dictionary {
                 guard let stringKey = (key as? NSString)?._swiftObject else {
                     continue
                 }
-                results[stringKey] = value
+                results[NSAttributedStringKey(stringKey)] = value
             }
             
             // Update effective range and return the results
@@ -211,15 +242,15 @@
         }
     }
     
-    func _attribute(_ attrName: String, atIndex location: Int, rangeInfo: RangeInfo) -> Any? {
+    func _attribute(_ attrName: NSAttributedStringKey, atIndex location: Int, rangeInfo: RangeInfo) -> Any? {
         var cfRange = CFRange()
         return withUnsafeMutablePointer(to: &cfRange) { (cfRangePointer: UnsafeMutablePointer<CFRange>) -> AnyObject? in
             // Get attribute value using CoreFoundation function
             let attribute: AnyObject?
             if rangeInfo.shouldFetchLongestEffectiveRange, let searchRange = rangeInfo.longestEffectiveRangeSearchRange {
-                attribute = CFAttributedStringGetAttributeAndLongestEffectiveRange(_cfObject, location, attrName._cfObject, CFRange(searchRange), cfRangePointer)
+                attribute = CFAttributedStringGetAttributeAndLongestEffectiveRange(_cfObject, location, attrName.rawValue._cfObject, CFRange(searchRange), cfRangePointer)
             } else {
-                attribute = CFAttributedStringGetAttribute(_cfObject, location, attrName._cfObject, cfRangePointer)
+                attribute = CFAttributedStringGetAttribute(_cfObject, location, attrName.rawValue._cfObject, cfRangePointer)
             }
             
             // Update effective range and return the result
@@ -241,18 +272,17 @@
         }
     }
     
-    func addAttributesToAttributeArray(attrs: [String : Any]?) {
+    func addAttributesToAttributeArray(attrs: [NSAttributedStringKey : Any]?) {
         guard _string.length > 0 else {
             return
         }
         
         let range = CFRange(location: 0, length: _string.length)
+        var attributes: [String : Any] = [:]
         if let attrs = attrs {
-            CFRunArrayInsert(_attributeArray, range, attrs._cfObject)
-        } else {
-            let emptyAttrs = [String : AnyObject]()
-            CFRunArrayInsert(_attributeArray, range, emptyAttrs._cfObject)
+            attrs.forEach { attributes[$0.rawValue] = $1 }
         }
+        CFRunArrayInsert(_attributeArray, range, attributes._cfObject)
     }
 }
 
@@ -277,19 +307,19 @@
 open class NSMutableAttributedString : NSAttributedString {
     
     open func replaceCharacters(in range: NSRange, with str: String) { NSUnimplemented() }
-    open func setAttributes(_ attrs: [String : Any]?, range: NSRange) { NSUnimplemented() }
+    open func setAttributes(_ attrs: [NSAttributedStringKey : Any]?, range: NSRange) { NSUnimplemented() }
     
     open var mutableString: NSMutableString {
         return _string as! NSMutableString
     }
-    
-    open func addAttribute(_ name: String, value: Any, range: NSRange) {
-        CFAttributedStringSetAttribute(_cfMutableObject, CFRange(range), name._cfObject, _SwiftValue.store(value))
+
+    open func addAttribute(_ name: NSAttributedStringKey, value: Any, range: NSRange) {
+        CFAttributedStringSetAttribute(_cfMutableObject, CFRange(range), name.rawValue._cfObject, _SwiftValue.store(value))
     }
+
+    open func addAttributes(_ attrs: [NSAttributedStringKey : Any], range: NSRange) { NSUnimplemented() }
     
-    open func addAttributes(_ attrs: [String : Any], range: NSRange) { NSUnimplemented() }
-    
-    open func removeAttribute(_ name: String, range: NSRange) { NSUnimplemented() }
+    open func removeAttribute(_ name: NSAttributedStringKey, range: NSRange) { NSUnimplemented() }
     
     open func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) { NSUnimplemented() }
     open func insert(_ attrString: NSAttributedString, at loc: Int) { NSUnimplemented() }
diff --git a/TestFoundation/TestNSAttributedString.swift b/TestFoundation/TestNSAttributedString.swift
index a1f2a35..5b08922 100644
--- a/TestFoundation/TestNSAttributedString.swift
+++ b/TestFoundation/TestNSAttributedString.swift
@@ -18,7 +18,6 @@
 #endif
 
 
-
 class TestNSAttributedString : XCTestCase {
     
     static var allTests: [(String, (TestNSAttributedString) -> () throws -> Void)] {
@@ -30,7 +29,7 @@
             ("test_enumerateAttributes", test_enumerateAttributes),
         ]
     }
-    
+
     func test_initWithString() {
         let string = "Lorem 😀 ipsum dolor sit amet, consectetur adipiscing elit. ⌘ Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit. ಠ_ರೃ"
         let attrString = NSAttributedString(string: string)
@@ -43,7 +42,7 @@
         XCTAssertEqual(range.length, string.utf16.count)
         XCTAssertEqual(attrs.count, 0)
 
-        let attribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
+        let attribute = attrString.attribute(NSAttributedStringKey("invalid"), at: 0, effectiveRange: &range)
         XCTAssertNil(attribute)
         XCTAssertEqual(range.location, 0)
         XCTAssertEqual(range.length, string.utf16.count)
@@ -51,7 +50,7 @@
     
     func test_initWithStringAndAttributes() {
         let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."
-        let attributes: [String : AnyObject] = ["attribute.placeholder.key" : "attribute.placeholder.value" as NSString]
+        let attributes: [NSAttributedStringKey : AnyObject] = [NSAttributedStringKey("attribute.placeholder.key") : "attribute.placeholder.value" as NSString]
         
         let attrString = NSAttributedString(string: string, attributes: attributes)
         XCTAssertEqual(attrString.string, string)
@@ -59,7 +58,7 @@
         
         var range = NSRange()
         let attrs = attrString.attributes(at: 0, effectiveRange: &range)
-        guard let value = attrs["attribute.placeholder.key"] as? String else {
+        guard let value = attrs[NSAttributedStringKey("attribute.placeholder.key")] as? String else {
             XCTAssert(false, "attribute value not found")
             return
         }
@@ -67,16 +66,16 @@
         XCTAssertEqual(range.length, attrString.length)
         XCTAssertEqual(value, "attribute.placeholder.value")
 
-        let invalidAttribute = attrString.attribute("invalid", at: 0, effectiveRange: &range)
+        let invalidAttribute = attrString.attribute(NSAttributedStringKey("invalid"), at: 0, effectiveRange: &range)
         XCTAssertNil(invalidAttribute)
         XCTAssertEqual(range.location, 0)
         XCTAssertEqual(range.length, string.utf16.count)
 
-        let attribute = attrString.attribute("attribute.placeholder.key", at: 0, effectiveRange: &range)
+        let attribute = attrString.attribute(NSAttributedStringKey("attribute.placeholder.key"), at: 0, effectiveRange: &range)
         XCTAssertEqual(range.location, 0)
         XCTAssertEqual(range.length, attrString.length)
         guard let validAttribute = attribute as? NSString else {
-            XCTAssert(false, "attribuet not found")
+            XCTAssert(false, "attribute not found")
             return
         }
         XCTAssertEqual(validAttribute, "attribute.placeholder.value")
@@ -85,7 +84,7 @@
     func test_longestEffectiveRange() {
         let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur et sem vitae consectetur. Nam venenatis lectus a laoreet blandit."
         
-        let attrKey = "attribute.placeholder.key"
+        let attrKey = NSAttributedStringKey("attribute.placeholder.key")
         let attrValue = "attribute.placeholder.value" as NSString
         
         let attrRange1 = NSRange(location: 0, length: 20)
@@ -110,12 +109,12 @@
     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 attrKey1 = NSAttributedStringKey("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 attrKey3 = NSAttributedStringKey("attribute.placeholder.key3")
         let attrValue3 = "attribute.placeholder.value3"
         let attrRange3 = NSRange(location: 40, length: 5)
         
@@ -161,15 +160,15 @@
 #else
         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 attrKey1 = NSAttributedStringKey("attribute.placeholder.key1")
         let attrValue1 = "attribute.placeholder.value1"
         let attrRange1 = NSRange(location: 0, length: 20)
         
-        let attrKey2 = "attribute.placeholder.key2"
+        let attrKey2 = NSAttributedStringKey("attribute.placeholder.key2")
         let attrValue2 = "attribute.placeholder.value2"
         let attrRange2 = NSRange(location: 18, length: 10)
         
-        let attrKey3 = "attribute.placeholder.key3"
+        let attrKey3 = NSAttributedStringKey("attribute.placeholder.key3")
         let attrValue3 = "attribute.placeholder.value3"
         let attrRange3 = NSRange(location: 40, length: 5)
         
@@ -235,9 +234,9 @@
         }
     }
     
-    fileprivate func describe(attrs: [String : Any]) -> String {
+    fileprivate func describe(attrs: [NSAttributedStringKey : Any]) -> String {
         if attrs.count > 0 {
-            return "[" + attrs.map({ "\($0):\($1)" }).sorted(by: { $0 < $1 }).joined(separator: ",") + "]"
+            return "[" + attrs.map({ "\($0.rawValue):\($1)" }).sorted(by: { $0 < $1 }).joined(separator: ",") + "]"
         } else {
             return "[:]"
         }