diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index 5e9f63127..3496b0122 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -145,8 +145,9 @@ open class TextStorage: NSTextStorage { private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { let stringWithAttachments = preprocessAttachmentsForInsertion(attributedString) + let preprocessedString = preprocessHeadingsForInsertion(stringWithAttachments) - return stringWithAttachments + return preprocessedString } /// Preprocesses an attributed string's attachments for insertion in the storage. @@ -211,6 +212,47 @@ open class TextStorage: NSTextStorage { return finalString } + /// Preprocesses an attributed string that is missing a `headingRepresentation` attribute for insertion in the storage. + /// + /// - Important: This method adds the `headingRepresentation` attribute if it determines the string should contain it. + /// This works around a problem where autocorrected text didn't contain the attribute. This may change in future versions. + /// + /// - Parameters: + /// - attributedString: the string we need to preprocess. + /// + /// - Returns: the preprocessed string. + /// + fileprivate func preprocessHeadingsForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { + // Ref. https://github.com/wordpress-mobile/AztecEditor-iOS/pull/1334 + + guard textStore.length > 0, attributedString.length > 0 else { + return attributedString + } + + // Get the attributes of the start of the current string in storage. + let currentAttrs = attributes(at: 0, effectiveRange: nil) + + guard + // the text currently in storage has a headingRepresentation key + let headerSize = currentAttrs[.headingRepresentation], + // the text coming in doesn't have a headingRepresentation key + attributedString.attribute(.headingRepresentation, at: 0, effectiveRange: nil) == nil, + // the text coming in has a paragraph style attribute + let paragraphStyle = attributedString.attributes(at: 0, effectiveRange: nil)[.paragraphStyle] as? ParagraphStyle, + // the paragraph style contains a property that's a Header type + paragraphStyle.properties.contains(where: { $0 is Header }) + else { + // Either the heading attribute wasn't present in the existing string, + // or the attributed string already had it. + return attributedString + } + + let processedString = NSMutableAttributedString(attributedString: attributedString) + processedString.addAttribute(.headingRepresentation, value: headerSize, range: attributedString.rangeOfEntireString) + + return processedString + } + fileprivate func detectAttachmentRemoved(in range: NSRange) { // Ref. https://github.com/wordpress-mobile/AztecEditor-iOS/issues/727: // If the delegate is not set, we *Explicitly* do not want to crash here. diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index b02ba55f3..27ea36ad1 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -1111,8 +1111,12 @@ open class TextView: UITextView { toggle(formatter: formatter, atRange: range) let liFormatter = LiFormatter(placeholderAttributes: typingAttributes) - toggle(formatter: liFormatter, atRange: range) + let isOlTagPresent = formatter.present(in: storage, at: range) + let isLiTagPresent = liFormatter.present(in: storage, at: range) + if isOlTagPresent != isLiTagPresent { + toggle(formatter: liFormatter, atRange: range) + } forceRedrawCursorAfterDelay() } @@ -1128,8 +1132,12 @@ open class TextView: UITextView { toggle(formatter: formatter, atRange: range) let liFormatter = LiFormatter(placeholderAttributes: typingAttributes) - toggle(formatter: liFormatter, atRange: range) - + let isOlTagPresent = formatter.present(in: storage, at: range) + let isLiTagPresent = liFormatter.present(in: storage, at: range) + + if isOlTagPresent != isLiTagPresent { + toggle(formatter: liFormatter, atRange: range) + } forceRedrawCursorAfterDelay() } diff --git a/AztecTests/TextKit/TextStorageTests.swift b/AztecTests/TextKit/TextStorageTests.swift index c80a35a36..9847e2fcb 100644 --- a/AztecTests/TextKit/TextStorageTests.swift +++ b/AztecTests/TextKit/TextStorageTests.swift @@ -579,4 +579,58 @@ class TextStorageTests: XCTestCase { let result = storage.getHTML() XCTAssertEqual(expectedResult, result) } + + /// Verifies that missing Heading attributes are retained on string replacements when appropriate + /// + func testMissingHeadingAttributeIsRetained() { + let formatter = HeaderFormatter(headerLevel: .h2) + storage.replaceCharacters(in: storage.rangeOfEntireString, with: "Hello i'm a header") + formatter.applyAttributes(to: storage, at: storage.rangeOfEntireString) + + let originalAttributes = storage.attributes(at: 0, effectiveRange: nil) + XCTAssertEqual(storage.string, "Hello i'm a header") + XCTAssertEqual(originalAttributes.count, 3) + XCTAssertNotNil(originalAttributes[.headingRepresentation]) + + let autoCorrectedAttributes = originalAttributes.filter { $0.key != .headingRepresentation } + + let autoCorrectedString = NSAttributedString( + string: "I'm", + attributes: autoCorrectedAttributes + ) + + let range = NSRange(location: 6, length: 3) + storage.replaceCharacters(in: range, with: autoCorrectedString) + + let finalAttributes = storage.attributes(at: range.location, effectiveRange: nil) + XCTAssertEqual(storage.string, "Hello I'm a header") + XCTAssertEqual(originalAttributes.keys, finalAttributes.keys) + } + + /// Verifies that converting a Heading to a Paragraph doesn't retain the heading attribute + /// + func testHeadingToParagraphDoesNotRetainHeadingAttribute() { + let headerFormatter = HeaderFormatter(headerLevel: .h2) + storage.replaceCharacters(in: storage.rangeOfEntireString, with: "Hello I'm a header") + headerFormatter.applyAttributes(to: storage, at: storage.rangeOfEntireString) + + let originalAttributes = storage.attributes(at: 0, effectiveRange: nil) + XCTAssertEqual(storage.string, "Hello I'm a header") + XCTAssertNotNil(originalAttributes[.headingRepresentation]) + + let paragraphAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 14), + .paragraphStyle: ParagraphStyle.default + ] + + let paragraphString = NSAttributedString( + string: "Hello I'm a paragraph", + attributes: paragraphAttributes + ) + storage.replaceCharacters(in: storage.rangeOfEntireString, with: paragraphString) + + let finalAttributes = storage.attributes(at: 0, effectiveRange: nil) + XCTAssertEqual(storage.string, "Hello I'm a paragraph") + XCTAssertNil(finalAttributes[.headingRepresentation]) + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c74771e..66e08e7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.19.8 +------- +* Fix Li tag when switching the list style. +* Retain Heading attribute when headings are autocorrected. + 1.19.7 ------- * Add variable to control whether typing attributes should be recalculated when deleting backward. diff --git a/WordPress-Aztec-iOS.podspec b/WordPress-Aztec-iOS.podspec index 8eacfefaa..b0e0e60a0 100644 --- a/WordPress-Aztec-iOS.podspec +++ b/WordPress-Aztec-iOS.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'WordPress-Aztec-iOS' - s.version = '1.19.7' + s.version = '1.19.8' s.summary = 'The native HTML Editor.' s.description = <<-DESC diff --git a/WordPress-Editor-iOS.podspec b/WordPress-Editor-iOS.podspec index 729a5c3b1..52fd2f2ee 100644 --- a/WordPress-Editor-iOS.podspec +++ b/WordPress-Editor-iOS.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'WordPress-Editor-iOS' - s.version = '1.19.7' + s.version = '1.19.8' s.summary = 'The WordPress HTML Editor.' s.description = <<-DESC