     ///
     func remove(from attributes: [String: Any]) -> [String: Any]
 
+    /// Checks if the attribute is present in a dictionary of attributes.
+    ///
+    func present(in attributes: [String: AnyObject]) -> Bool
+
+    /// Checks if the attribute is present in a given Attributed String at the specified index.
+    ///
+    func present(in text: NSAttributedString, at index: Int) -> Bool
+
     /// The range to apply the attributes to.
     ///
     /// By default, this returns the passed `range`, but implementations of this

    /// different range (e.g. a paragraph)
    ///
    func applicationRange(for range: NSRange, in string: NSAttributedString) -> NSRange

    /// Toggles an attribute in the specified range of a text storage.
    ///
    /// The application range might be different than the passed range, as
    /// explained in `applicationRange(for:in:)`
    ///
    func toggleAttribute(in text: NSMutableAttributedString, at range: NSRange)
}

extension AttributeFormatter {
    /// Checks if the attribute is present in a text view at the specified index.
    ///
    func present(in storage: NSTextStorage, at index: Int) -> Bool {
        let safeIndex = max(min(index, storage.length - 1), 0)
        let attributes = storage.attributes(at: safeIndex, effectiveRange: nil) as [String : AnyObject]
        return present(in: attributes)
    }
}

// MARK: - Default implementations

extension AttributeFormatter {
    func applicationRange(for range: NSRange, in string: NSAttributedString) -> NSRange {
        return range
    }

    /// The string to be used when adding attributes to an empty line.
    ///
    var placeholderForAttributedEmptyLine: String {
        return "\u{200B}"
    }
}

// MARK: - Private methods

private extension AttributeFormatter {

    func toggleAttribute(in string: NSMutableAttributedString, at range: NSRange) {
        if attribute(inString: string, at: range.location) {
            removeAttributes(fromString: string, at: range)
        } else {
            applyAttributes(toString: string, at: range)
        }
    }

    func applyAttributes(toString string: NSMutableAttributedString, at range: NSRange) {
        let currentAttributes = string.attributes(at: range.location, effectiveRange: nil)
        let attributes = apply(to: currentAttributes)
        string.addAttributes(attributes, range: range)
    }

    func removeAttributes(fromString string: NSMutableAttributedString, at range: NSRange) {
        let currentAttributes = string.attributes(at: range.location, effectiveRange: nil)
        let attributes = remove(from: currentAttributes)
        string.addAttributes(attributes, range: range)
    }

    func attribute(inString string: NSAttributedString, at index: Int) -> Bool {
        let attributes = string.attributes(at: index, effectiveRange: nil)
        return present(in: attributes as [String : AnyObject])
    }

    func insertEmptyAttribute(inString string: NSMutableAttributedString, at index: Int) {
        let attributes = apply(to: [:])
        let attributedSpace = NSAttributedString(string: placeholderForAttributedEmptyLine, attributes: attributes)
        string.insert(attributedSpace, at: index)
    }
}

// MARK: - Attribute Formater types

protocol CharacterAttributeFormatter: AttributeFormatter {
}

protocol ParagraphAttributeFormatter: AttributeFormatter {
}


        return string.paragraphRange(for: range)
    }

    func toggle(in text: NSMutableAttributedString, at range: NSRange) {
        let applicationRange = self.applicationRange(for: range, in: text)

        if applicationRange.length == 0 || text.length == 0 {
            insertEmptyAttribute(in: text, at: applicationRange.location)
        }

        toggleAttributes(in: text, at: applicationRange)
    }
}
