 
         beginEditing()
 
-        if mustUpdateDOM() {
-            replaceCharactersInDOM(in: range, with: str)
-        }
-
         detectAttachmentRemoved(in: range)
         textStore.replaceCharacters(in: range, with: str)
 

    override open func setAttributes(_ attrs: [String : Any]?, range: NSRange) {
        beginEditing()

        if mustUpdateDOM() && allowFixingDOMAttributes && range.length > 0, let attributes = attrs {
            applyStylesToDom(attributes: attributes, in: range)
        }

        textStore.setAttributes(attrs, range: range)
        edited(.editedAttributes, range: range, changeInLength: 0)
        
        endEditing()

        print("Style: \(dom.getHTML())")
    }

    /// This override exists to prevent text replacement from propagating style-changes to the DOM
    /// This should not be a problem in our logic because the DOM is smart enough to update its
    /// style after text modifications.
    ///
    /// This was causing issues specifically with lists.  To understand why, just comment this
    /// method and run the unit tests.
    ///
    override open func endEditing() {
        allowFixingDOMAttributes = false
        super.endEditing()
        allowFixingDOMAttributes = true
    }

    // MARK: - DOM: Replacing Characters

    private func replaceCharactersInDOM(in range: NSRange, with str: String) {

        guard let swiftRange = string.nsRange(fromUTF16NSRange: range) else {
            fatalError()
        }

        let targetDomRange = string.map(visualUTF16Range: swiftRange)

        if targetDomRange.length > 0 || str.characters.count > 0 {
            dom.replaceCharacters(inRange: targetDomRange, withString: str)
        }
    }

    private func replaceCharactersInDOM(in range: NSRange, with attrString: NSAttributedString) {

        let targetDomRange = string.map(visualUTF16Range: range)

        guard let swiftRange = string.nsRange(fromUTF16NSRange: range) else {
            fatalError()
        }

        let domString = NSAttributedString(with: attrString, replacingOcurrencesOf: String(.paragraphSeparator), with: "")

        if targetDomRange.length > 0 || domString.length > 0 {
            dom.replaceCharacters(inRange: targetDomRange, withString: domString.string)
        }

        if attrString.string == String(.paragraphSeparator) {
            dom.addBlockSeparator(at: targetDomRange.location)
        }

        if targetDomRange.length != swiftRange.length {
            dom.deleteBlockSeparator(at: targetDomRange.location)
        }

        print("Pre: \(dom.getHTML())")
        if domString.length > 0 {
            applyStylesToDom(from: domString, startingAt: range.location)
        }
        print("Pos: \(dom.getHTML())")
    }

    // MARK: - DOM: Applying Styles

    /// This method applies the styles in the specified attributes dictionary, to the DOM in the
    /// specified range.  To do so, it calculates the differences and applies them.
    ///
    /// - Parameters:
    ///     - attributes: the attributes to apply.
    ///     - range: the range to apply the styles to.
    ///
    private func applyStylesToDom(attributes: [String : Any], in range: NSRange) {

        let canMergeLeft = range.location > 0 ? !textStore.string.isStartOfNewLine(atUTF16Offset: range.location) : false
        let canMergeRight = range.location + range.length < textStore.length - 1 ? !textStore.string.isEndOfLine(atUTF16Offset: range.location + range.length) : false

        textStore.enumerateAttributeDifferences(in: range, against: attributes, do: { (subRange, key, sourceValue, targetValue) in

            let domRange = textStore.string.map(visualUTF16Range: subRange)

            guard domRange.length > 0 else {
                return
            }

            processAttributesDifference(in: domRange, key: key, sourceValue: sourceValue, targetValue: targetValue, canMergeLeft: canMergeLeft, canMergeRight: canMergeRight)
        })
    }

    /// This method applies the styles from the specified attributed string, to the DOM, starting
    /// at the specified location, and moving ahead for the length of the attributed string.
    ///
    /// This makes sense only if the attributed string has already been added to the DOM, as a way
    /// to apply the styles for that string.
    ///
    /// - Parameters:
    ///     - attributedString: the attributed string containing the styles we want to apply.
    ///     - location: the starting location where the styles should be applied in the DOM.
    ///         It's the offset this method will use to apply the styles found in the source string.
    ///
    private func applyStylesToDom(from attributedString: NSAttributedString, startingAt location: Int) {
        let originalAttributes = [String:Any]()
        let fullRange = NSRange(location: 0, length: attributedString.length)

        let domLocation = textStore.string.map(visualRange: NSRange(location: location, length: 0)).location

        let canMergeLeft = location > 0 ? !textStore.string.isStartOfNewLine(atUTF16Offset: location) : false
        let canMergeRight = location < textStore.length - 1 ? !textStore.string.isEndOfLine(atUTF16Offset: location) : false

        attributedString.enumerateAttributeDifferences(in: fullRange, against: originalAttributes, do: { (subRange, key, sourceValue, targetValue) in
            // The source and target values are inverted since we're enumerating on the new string.

            let domRange = NSRange(location: domLocation + subRange.location, length: subRange.length)

            guard let swiftDomRange = dom.string().nsRange(fromUTF16NSRange: domRange) else {
                // This should not be possible, but if this ever happens in production it's better to lose
                // the style than it is to crash the editor.
                //
                assertionFailure("Unexpected range conversion problem.")
                return
            }

            processAttributesDifference(in: swiftDomRange, key: key, sourceValue: targetValue, targetValue: sourceValue, canMergeLeft: canMergeLeft, canMergeRight: canMergeRight)
        })
    }

    // MARK: - DOM: Calculating and Applying Style Differences

    /// Check the difference in styles and applies the necessary changes to the DOM string.
    ///
    /// - Parameters:
    ///   - domRange: the range to check
    ///   - key: the attribute style key
    ///   - sourceValue: the original value of the attribute
    ///   - targetValue: the new value of the attribute
    ///
    private func processAttributesDifference(
        in domRange: NSRange,
        key: String,
        sourceValue: Any?,
        targetValue: Any?,
        canMergeLeft: Bool = true,
        canMergeRight: Bool = true) {

        let isCommentAttachment = sourceValue is CommentAttachment || targetValue is CommentAttachment
        let isHtmlAttachment = sourceValue is HTMLAttachment || targetValue is HTMLAttachment
        let isLineAttachment = sourceValue is LineAttachment || targetValue is LineAttachment
        let isImageAttachment = sourceValue is ImageAttachment || targetValue is ImageAttachment
        let isVideoAttachment = sourceValue is VideoAttachment || targetValue is VideoAttachment

        switch(key) {
        case NSFontAttributeName:
            let sourceFont = sourceValue as? UIFont
            let targetFont = targetValue as? UIFont

            processFontDifferences(in: domRange, betweenOriginal: sourceFont, andNew: targetFont)
        case NSStrikethroughStyleAttributeName:
            let sourceStyle = sourceValue as? NSNumber
            let targetStyle = targetValue as? NSNumber

            processStrikethroughDifferences(in: domRange, betweenOriginal: sourceStyle, andNew: targetStyle)
        case NSUnderlineStyleAttributeName:
            let sourceStyle = sourceValue as? NSNumber
            let targetStyle = targetValue as? NSNumber

            processUnderlineDifferences(in: domRange, betweenOriginal: sourceStyle, andNew: targetStyle)
        case NSAttachmentAttributeName where isLineAttachment:
            let sourceAttachment = sourceValue as? LineAttachment
            let targetAttachment = targetValue as? LineAttachment

            processLineAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
        case NSAttachmentAttributeName where isCommentAttachment:
            let sourceAttachment = sourceValue as? CommentAttachment
            let targetAttachment = targetValue as? CommentAttachment

            processCommentAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
        case NSAttachmentAttributeName where isHtmlAttachment:
            let sourceAttachment = sourceValue as? HTMLAttachment
            let targetAttachment = targetValue as? HTMLAttachment

            processHtmlAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
        case NSAttachmentAttributeName where isImageAttachment:
            let sourceAttachment = sourceValue as? ImageAttachment
            let targetAttachment = targetValue as? ImageAttachment

            processImageAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
        case NSAttachmentAttributeName where isVideoAttachment:
            let sourceAttachment = sourceValue as? VideoAttachment
            let targetAttachment = targetValue as? VideoAttachment

            processVideoAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
        case NSParagraphStyleAttributeName:
            let sourceStyle = sourceValue as? ParagraphStyle
            let targetStyle = targetValue as? ParagraphStyle

            processBlockquoteDifferences(in: domRange, betweenOriginal: sourceStyle?.blockquote, andNew: targetStyle?.blockquote)
            processListDifferences(in: domRange, betweenOriginal: sourceStyle?.textLists.last, andNew: targetStyle?.textLists.last, canMergeLeft: canMergeLeft, canMergeRight: canMergeRight)
            processHeaderDifferences(in: domRange, betweenOriginal: sourceStyle?.headerLevel, andNew: targetStyle?.headerLevel)
            processHTMLParagraphDifferences(in: domRange, betweenOriginal: sourceStyle?.htmlParagraph, andNew: targetStyle?.htmlParagraph)
        case NSLinkAttributeName:
            let sourceStyle = sourceValue as? URL
            let targetStyle = targetValue as? URL
            processLinkDifferences(in: domRange, betweenOriginal: sourceStyle, andNew: targetStyle)
        default:
            break
        }
    }

    /// Processes differences in a font object, and applies them to the DOM in the specified range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalFont: the original font object.
    ///     - newFont: the new font object.
    ///
    private func processFontDifferences(in range: NSRange, betweenOriginal originalFont: UIFont?, andNew newFont: UIFont?) {
        processBoldDifferences(in: range, betweenOriginal: originalFont, andNew: newFont)
        processItalicDifferences(in: range, betweenOriginal: originalFont, andNew: newFont)
    }

    /// Processes differences in the bold trait of two font objects, and applies them to the DOM in
    /// the specified range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalFont: the original font object.
    ///     - newFont: the new font object.
    ///
    private func processBoldDifferences(in range: NSRange, betweenOriginal originalFont: UIFont?, andNew newFont: UIFont?) {
        let oldIsBold = originalFont?.containsTraits(.traitBold) ?? false
        let newIsBold = newFont?.containsTraits(.traitBold) ?? false

        let addBold = !oldIsBold && newIsBold
        let removeBold = oldIsBold && !newIsBold

        if addBold {
            dom.applyBold(spanning: range)
        } else if removeBold {
            dom.removeBold(spanning: range)
        }
    }

    /// Process difference in attachmente properties, and applies them to the DOM in the specified range
    ///
    /// - Parameters:
    ///   - range: the range in the DOM where the differences must be applied.
    ///   - original: the original attachment existing in the range if any.
    ///   - new: the new attachment to apply to the range if any.
    ///
    private func processImageAttachmentDifferences(in range: NSRange, betweenOriginal original: ImageAttachment?, andNew new: ImageAttachment?) {

        let originalUrl = original?.url
        let newUrl = new?.url

        let addImageUrl = originalUrl == nil && newUrl != nil
        let removeImageUrl = originalUrl != nil && newUrl == nil

        if addImageUrl {
            guard let urlToAdd = newUrl else {
                assertionFailure("This should not be possible.  Review your logic.")
                return
            }

            dom.replace(range, with: urlToAdd)
        } else if removeImageUrl {
            dom.removeImage(spanning: range)
        }
    }

    /// Process difference in attachmente properties, and applies them to the DOM in the specified range
    ///
    /// - Parameters:
    ///   - range: the range in the DOM where the differences must be applied.
    ///   - original: the original attachment existing in the range if any.
    ///   - new: the new attachment to apply to the range if any.
    ///
    private func processVideoAttachmentDifferences(in range: NSRange, betweenOriginal original: VideoAttachment?, andNew new: VideoAttachment?) {

        let originalUrl = original?.srcURL
        let newUrl = new?.srcURL

        let addVideoUrl = originalUrl == nil && newUrl != nil
        let removeVideoUrl = originalUrl != nil && newUrl == nil

        if addVideoUrl {
            guard let urlToAdd = newUrl else {
                assertionFailure("This should not be possible.  Review your logic.")
                return
            }

            dom.replace(range, withVideoURL: urlToAdd, posterURL: new?.posterURL)
        } else if removeVideoUrl {
            dom.removeVideo(spanning: range)
        }
    }

    private func processLineAttachmentDifferences(in range: NSRange, betweenOriginal original: LineAttachment?, andNew new: LineAttachment?) {

        dom.replaceWithHorizontalRuler(range)
    }

    private func processCommentAttachmentDifferences(in range: NSRange, betweenOriginal original: CommentAttachment?, andNew new: CommentAttachment?) {
        guard let newAttachment = new else {
            return
        }

        dom.replace(range, withComment: newAttachment.text)
    }

    private func processHtmlAttachmentDifferences(in range: NSRange, betweenOriginal original: HTMLAttachment?, andNew new: HTMLAttachment?) {
        guard let html = new?.rawHTML, original?.rawHTML != new?.rawHTML else {
            return
        }

        dom.replace(range, withRawHTML: html)
    }

    /// Processes differences in blockquote styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalStyle: the original Blockquote object if any.
    ///     - newStyle: the new Blockquote object.
    ///
    private func processHTMLParagraphDifferences(in range: NSRange, betweenOriginal originalStyle: HTMLParagraph?, andNew newStyle: HTMLParagraph?) {

        let addStyle = originalStyle == nil && newStyle != nil
        let removeStyle = originalStyle != nil && newStyle == nil

        if addStyle {
            dom.applyHTMLParagraph(spanning: range)
        } else if removeStyle {
            dom.removeHTMLParagraph(spanning: range)
        }
    }


    /// Processes differences in the italic trait of two font objects, and applies them to the DOM
    /// in the specified range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalFont: the original font object.
    ///     - newFont: the new font object.
    ///
    private func processItalicDifferences(in range: NSRange, betweenOriginal originalFont: UIFont?, andNew newFont: UIFont?) {
        let oldIsItalic = originalFont?.containsTraits(.traitItalic) ?? false
        let newIsItalic = newFont?.containsTraits(.traitItalic) ?? false

        let addItalic = !oldIsItalic && newIsItalic
        let removeItalic = oldIsItalic && !newIsItalic

        if addItalic {
            dom.applyItalic(spanning: range)
        } else if removeItalic {
            dom.removeItalic(spanning: range)
        }
    }

    /// Processes differences in two strikethrough styles, and applies them to the DOM in the
    /// specified range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalFont: the original font object.
    ///     - newFont: the new font object.
    ///
    private func processStrikethroughDifferences(in range: NSRange, betweenOriginal originalStyle: NSNumber?, andNew newStyle: NSNumber?) {

        let sourceStyle = originalStyle ?? 0
        let targetStyle = newStyle ?? 0

        // At some point we'll support different styles.  For now we only check if ANY style is
        // set.
        //
        let addStyle = sourceStyle == 0 && targetStyle == 1
        let removeStyle = sourceStyle == 1 && targetStyle == 0

        if addStyle {
            dom.applyStrikethrough(spanning: range)
        } else if removeStyle {
            dom.removeStrikethrough(spanning: range)
        }
    }

    /// Processes differences in two underline styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalFont: the original font object.
    ///     - newFont: the new font object.
    ///
    private func processUnderlineDifferences(in range: NSRange, betweenOriginal originalStyle: NSNumber?, andNew newStyle: NSNumber?) {

        let sourceStyle = originalStyle ?? 0
        let targetStyle = newStyle ?? 0

        // At some point we'll support different styles.  For now we only check if ANY style is
        // set.
        //
        let addStyle = sourceStyle == 0 && targetStyle == 1
        let removeStyle = sourceStyle == 1 && targetStyle == 0

        if addStyle {
            dom.applyUnderline(spanning: range)
        } else if removeStyle {
            dom.removeUnderline(spanning: range)
        }
    }

    /// Processes differences in blockquote styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalStyle: the original Blockquote object if any.
    ///     - newStyle: the new Blockquote object.
    ///
    private func processBlockquoteDifferences(in range: NSRange, betweenOriginal originalStyle: Blockquote?, andNew newStyle: Blockquote?) {

        let addStyle = originalStyle == nil && newStyle != nil
        let removeStyle = originalStyle != nil && newStyle == nil

        if addStyle {
            dom.applyBlockquote(spanning: range)
        } else if removeStyle {
            dom.removeBlockquote(spanning: range)
        }
    }

    /// Processes differences in list styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalStyle: the original TextList object if any.
    ///     - newStyle: the new Blockquote object.
    ///
    private func processListDifferences(in range: NSRange, betweenOriginal originalStyle: TextList?, andNew newStyle: TextList?, canMergeLeft: Bool = false, canMergeRight: Bool = false) {

        let original = originalStyle?.style
        let new = newStyle?.style

        guard original != new else {
            return
        }

        let removeOrdered = original == .ordered
        let removeUnordered = original == .unordered
        let addOrdered = new == .ordered
        let addUnordered = new == .unordered

        var rangeForAddingStyle = range

        if removeOrdered {
            rangeForAddingStyle = dom.removeOrderedList(spanning: range)
        } else if removeUnordered {
            rangeForAddingStyle = dom.removeUnorderedList(spanning: range)
        }

        if addOrdered {
            dom.applyOrderedList(spanning: rangeForAddingStyle, canMergeLeft: canMergeLeft, canMergeRight: canMergeRight)
        } else if addUnordered {
            dom.applyUnorderedList(spanning: rangeForAddingStyle, canMergeLeft: canMergeLeft, canMergeRight: canMergeRight)
        }
    }

    /// Processes differences in header styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalHeaderLevel: the original font object.
    ///     - newHeaderLevel: the new font object.
    ///
    private func processHeaderDifferences(in range: NSRange, betweenOriginal originalHeaderLevel: Int?, andNew newHeaderLevel: Int?) {

        let sourceHeader = originalHeaderLevel ?? 0
        let targetHeader = newHeaderLevel ?? 0

        let addStyle = sourceHeader >= 0 && targetHeader > 0 && sourceHeader != targetHeader
        let removeStyle = sourceHeader > 0 && targetHeader >= 0 && sourceHeader != targetHeader

        if addStyle {
            dom.applyHeader(targetHeader, spanning: range)
        }
        if removeStyle {
            dom.removeHeader(sourceHeader, spanning: range)
        }
    }

    /// Processes differences in link styles, and applies them to the DOM in the specified
    /// range.
    ///
    /// - Parameters:
    ///     - range: the range in the DOM where the differences must be applied.
    ///     - originalURL: the original link URL object if any.
    ///     - newURL: the new link URL object if any.
    ///
    private func processLinkDifferences(in range: NSRange, betweenOriginal originalURL: URL?, andNew newURL: URL?) {

        let addStyle = originalURL == nil && newURL != nil
        let removeStyle = originalURL != nil && newURL == nil

        if addStyle {            
            dom.applyLink(newURL, spanning: range)
        } else if removeStyle {
            dom.removeLink(spanning: range)
        }
    }
    
    // MARK: - Styles: Toggling

    @discardableResult

        let preprocessedString = preprocessAttributesForInsertion(attrString)

        beginEditing()

        detectAttachmentRemoved(in: range)
        textStore.replaceCharacters(in: range, with: preprocessedString)
