From d6554516c81f08184ddffc0c51cb4b03eb70fa3c Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 14 Mar 2022 09:17:34 +0100 Subject: [PATCH 01/13] Mark Formatting - Fix autocorrected strings and changing the caret position bug --- Aztec.xcodeproj/project.pbxproj | 4 ++ .../GenericElementConverter.swift | 2 + .../MarkStringAttributeConverter.swift | 42 +++++++++++++++++++ .../Implementations/MarkFormatter.swift | 2 +- .../Conversions/AttributedStringParser.swift | 1 + Aztec/Classes/TextKit/TextStorage.swift | 38 ++++++++++++++++- Aztec/Classes/TextKit/TextView.swift | 2 + AztecTests/TextKit/TextStorageTests.swift | 27 ++++++++++++ 8 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift diff --git a/Aztec.xcodeproj/project.pbxproj b/Aztec.xcodeproj/project.pbxproj index 5042b71ff..a76e24ede 100644 --- a/Aztec.xcodeproj/project.pbxproj +++ b/Aztec.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 40A2986D1FD61B0C00AEDF3B /* ElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A2986C1FD61B0C00AEDF3B /* ElementConverter.swift */; }; 40A298711FD61B6F00AEDF3B /* ImageElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A298701FD61B6F00AEDF3B /* ImageElementConverter.swift */; }; 40A298731FD61E1900AEDF3B /* VideoElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A298721FD61E1900AEDF3B /* VideoElementConverter.swift */; }; + 5608841E27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */; }; 568FF25827552BFF0057B2E3 /* MarkFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568FF25727552BFF0057B2E3 /* MarkFormatter.swift */; }; 594C9D6F1D8BE61F00D74542 /* Aztec.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5951CB8E1D8BC93600E1866F /* Aztec.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 594C9D731D8BE6C300D74542 /* InAttributeConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FEA06B1D8BDFA700D138DF /* InAttributeConverterTests.swift */; }; @@ -288,6 +289,7 @@ 40A298701FD61B6F00AEDF3B /* ImageElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageElementConverter.swift; sourceTree = ""; }; 40A298721FD61E1900AEDF3B /* VideoElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoElementConverter.swift; sourceTree = ""; }; 50A1CC6E250FEA93001D5517 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; + 5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkStringAttributeConverter.swift; sourceTree = ""; }; 568FF25727552BFF0057B2E3 /* MarkFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkFormatter.swift; sourceTree = ""; }; 5951CB8E1D8BC93600E1866F /* Aztec.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Aztec.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5951CB921D8BC93600E1866F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1101,6 +1103,7 @@ F15BA60C215159A600424120 /* ItalicStringAttributeConverter.swift */, FF94935D245738AC0085ABB3 /* SuperscriptStringAttributeConverter.swift */, FF949361245744090085ABB3 /* SubscriptStringAttributeConverter.swift */, + 5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */, F15BA60E21515C0F00424120 /* UnderlineStringAttributeConverter.swift */, ); path = Implementations; @@ -1547,6 +1550,7 @@ F1584794203C94AC00EE05A1 /* Dictionary+AttributedStringKey.swift in Sources */, B572AC281E817CFE008948C2 /* CommentAttachment.swift in Sources */, F1E1D5881FEC52EE0086B339 /* GenericElementConverter.swift in Sources */, + 5608841E27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift in Sources */, F1FA0E861E6EF514009D98EE /* Node.swift in Sources */, FFD3C1712344DB4E00AE8DA0 /* ForegroundColorCSSAttributeMatcher.swift in Sources */, FFD3C1732344DCA900AE8DA0 /* ForegroundColorElementAttributeConverter.swift in Sources */, diff --git a/Aztec/Classes/Converters/ElementsToAttributedString/Implementations/GenericElementConverter.swift b/Aztec/Classes/Converters/ElementsToAttributedString/Implementations/GenericElementConverter.swift index aca55ab0c..62ef6fd7d 100644 --- a/Aztec/Classes/Converters/ElementsToAttributedString/Implementations/GenericElementConverter.swift +++ b/Aztec/Classes/Converters/ElementsToAttributedString/Implementations/GenericElementConverter.swift @@ -36,6 +36,7 @@ class GenericElementConverter: ElementConverter { lazy var liFormatter = LiFormatter() lazy var superscriptFormatter = SuperscriptFormatter() lazy var subscriptFormatter = SubscriptFormatter() + lazy var markFormatter = MarkFormatter() public lazy var elementFormattersMap: [Element: AttributeFormatter] = { return [ @@ -60,6 +61,7 @@ class GenericElementConverter: ElementConverter { .li: self.liFormatter, .sup: self.superscriptFormatter, .sub: self.subscriptFormatter, + .mark: self.markFormatter, ] }() diff --git a/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift new file mode 100644 index 000000000..14be4b5bc --- /dev/null +++ b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift @@ -0,0 +1,42 @@ +import Foundation +import UIKit + + +/// Converts the mark style information from string attributes and aggregates it into an +/// existing array of element nodes. +/// +open class MarkStringAttributeConverter: StringAttributeConverter { + + private let toggler = HTMLStyleToggler(defaultElement: .mark, cssAttributeMatcher: ForegroundColorCSSAttributeMatcher()) + + public func convert( + attributes: [NSAttributedString.Key: Any], + andAggregateWith elementNodes: [ElementNode]) -> [ElementNode] { + + var elementNodes = elementNodes + + // We add the representation right away, if it exists... as it could contain attributes beyond just this + // style. The enable and disable methods below can modify this as necessary. + // + + if let elementNode = attributes.storedElement(for: NSAttributedString.Key.markHtmlRepresentation) { + elementNodes.append(elementNode) + } + + if shouldEnableMarkElement(for: attributes) { + return toggler.enable(in: elementNodes) + } else { + return toggler.disable(in: elementNodes) + } + } + + // MARK: - Style Detection + + func shouldEnableMarkElement(for attributes: [NSAttributedString.Key: Any]) -> Bool { + return isMark(for: attributes) + } + + func isMark(for attributes: [NSAttributedString.Key: Any]) -> Bool { + return attributes[.markHtmlRepresentation] != nil + } +} diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index 61da0a621..3022db0f8 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -24,8 +24,8 @@ class MarkFormatter: AttributeFormatter { func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] { var resultingAttributes = attributes + resultingAttributes[.foregroundColor] = placeholderAttributes![.foregroundColor] resultingAttributes.removeValue(forKey: .markHtmlRepresentation) - return resultingAttributes } diff --git a/Aztec/Classes/NSAttributedString/Conversions/AttributedStringParser.swift b/Aztec/Classes/NSAttributedString/Conversions/AttributedStringParser.swift index e64f39e3d..5c2d5501c 100644 --- a/Aztec/Classes/NSAttributedString/Conversions/AttributedStringParser.swift +++ b/Aztec/Classes/NSAttributedString/Conversions/AttributedStringParser.swift @@ -29,6 +29,7 @@ class AttributedStringParser { UnderlineStringAttributeConverter(), SuperscriptStringAttributeConverter(), SubscriptStringAttributeConverter(), + MarkStringAttributeConverter(), ] // MARK: - Attachment Converters diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index 1abfc7f50..e6941e025 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -145,7 +145,8 @@ open class TextStorage: NSTextStorage { private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { let stringWithAttachments = preprocessAttachmentsForInsertion(attributedString) - let preprocessedString = preprocessHeadingsForInsertion(stringWithAttachments) + let stringWithHeadings = preprocessHeadingsForInsertion(stringWithAttachments) + let preprocessedString = preprocessMarkForInsertion(stringWithHeadings) return preprocessedString } @@ -253,6 +254,41 @@ open class TextStorage: NSTextStorage { return processedString } + /// Preprocesses an attributed string that is missing a `markHtmlRepresentation` attribute for insertion in the storage, this reuses the same behavior as preprocessHeadingsForInsertion + /// + /// - Important: This method adds the `markHtmlRepresentation` 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 preprocessMarkForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { + 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 markHtmlRepresentation key + let markSize = currentAttrs[.markHtmlRepresentation], + // the text coming in doesn't have a markHtmlRepresentation key + attributedString.attribute(.markHtmlRepresentation, at: 0, effectiveRange: nil) == nil + else { + // Either the mark attribute wasn't present in the existing string, + // or the attributed string already had it. + return attributedString + } + + let processedString = NSMutableAttributedString(attributedString: attributedString) + processedString.addAttribute(.markHtmlRepresentation, value: markSize, 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 2d83526bb..47157f122 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -1160,6 +1160,8 @@ open class TextView: UITextView { let formatter = MarkFormatter() formatter.placeholderAttributes = self.defaultAttributes toggle(formatter: formatter, atRange: range) + + formattingDelegate?.textViewCommandToggledAStyle() } /// Replaces with an horizontal ruler on the specified range diff --git a/AztecTests/TextKit/TextStorageTests.swift b/AztecTests/TextKit/TextStorageTests.swift index 9847e2fcb..0b9712e95 100644 --- a/AztecTests/TextKit/TextStorageTests.swift +++ b/AztecTests/TextKit/TextStorageTests.swift @@ -633,4 +633,31 @@ class TextStorageTests: XCTestCase { XCTAssertEqual(storage.string, "Hello I'm a paragraph") XCTAssertNil(finalAttributes[.headingRepresentation]) } + + /// Verifies that missing Mark attributes are retained on string replacements when appropriate + /// + func testMissingMarkAttributeIsRetained() { + let formatter = MarkFormatter() + storage.replaceCharacters(in: storage.rangeOfEntireString, with: "Hello i'm a text highlighted") + formatter.applyAttributes(to: storage, at: storage.rangeOfEntireString) + + let originalAttributes = storage.attributes(at: 0, effectiveRange: nil) + XCTAssertEqual(storage.string, "Hello i'm a text highlighted") + XCTAssertEqual(originalAttributes.count, 2) + XCTAssertNotNil(originalAttributes[.markHtmlRepresentation]) + + let autoCorrectedAttributes = originalAttributes.filter { $0.key != .markHtmlRepresentation } + + 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 text highlighted") + XCTAssertEqual(originalAttributes.keys, finalAttributes.keys) + } } From af4c4635a2cba9825cedefff90512cdf13b24d43 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 14 Mar 2022 18:43:50 +0100 Subject: [PATCH 02/13] Mark formatter - Fix autocorrected formatting --- Aztec/Classes/TextKit/TextStorage.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index e6941e025..f0a6a364f 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -143,10 +143,10 @@ open class TextStorage: NSTextStorage { // MARK: - NSAttributedString preprocessing - private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { + private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString { let stringWithAttachments = preprocessAttachmentsForInsertion(attributedString) let stringWithHeadings = preprocessHeadingsForInsertion(stringWithAttachments) - let preprocessedString = preprocessMarkForInsertion(stringWithHeadings) + let preprocessedString = preprocessMarkForInsertion(stringWithHeadings, range) return preprocessedString } @@ -264,13 +264,18 @@ open class TextStorage: NSTextStorage { /// /// - Returns: the preprocessed string. /// - fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString { - guard textStore.length > 0, attributedString.length > 0 else { + fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString { + guard textStore.length > 0, attributedString.length > 0, range.endLocation != range.lowerBound else { return attributedString } + var startAt = 0 + + if range.endLocation > range.lowerBound { + startAt = range.lowerBound + } // Get the attributes of the start of the current string in storage. - let currentAttrs = attributes(at: 0, effectiveRange: nil) + let currentAttrs = attributes(at: startAt, effectiveRange: nil) guard // the text currently in storage has a markHtmlRepresentation key @@ -341,7 +346,7 @@ open class TextStorage: NSTextStorage { override open func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) { - let preprocessedString = preprocessAttributesForInsertion(attrString) + let preprocessedString = preprocessAttributesForInsertion(attrString, range) beginEditing() From dd8a325d62efe0856c9c4b25bb3f36adcbb1a872 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 8 Jan 2024 17:42:48 +0100 Subject: [PATCH 03/13] Remove setting the color to the placeholder attributes --- Aztec/Classes/Formatters/Implementations/MarkFormatter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index 3022db0f8..0fe302846 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -24,7 +24,6 @@ class MarkFormatter: AttributeFormatter { func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] { var resultingAttributes = attributes - resultingAttributes[.foregroundColor] = placeholderAttributes![.foregroundColor] resultingAttributes.removeValue(forKey: .markHtmlRepresentation) return resultingAttributes } From 87b3cd8223621f6a5963df8fb695f6de5dacb463 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 8 Jan 2024 17:43:57 +0100 Subject: [PATCH 04/13] Update preprocessMarkForInsertion --- Aztec/Classes/TextKit/TextStorage.swift | 72 ++++++++++++++++--------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index f0a6a364f..867a769f1 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -265,33 +265,23 @@ open class TextStorage: NSTextStorage { /// - Returns: the preprocessed string. /// fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString { - guard textStore.length > 0, attributedString.length > 0, range.endLocation != range.lowerBound else { + // If the attributed string is empty, return as is + if attributedString.length == 0 { return attributedString } - var startAt = 0 - - if range.endLocation > range.lowerBound { - startAt = range.lowerBound - } - - // Get the attributes of the start of the current string in storage. - let currentAttrs = attributes(at: startAt, effectiveRange: nil) - - guard - // the text currently in storage has a markHtmlRepresentation key - let markSize = currentAttrs[.markHtmlRepresentation], - // the text coming in doesn't have a markHtmlRepresentation key - attributedString.attribute(.markHtmlRepresentation, at: 0, effectiveRange: nil) == nil - else { - // Either the mark attribute wasn't present in the existing string, - // or the attributed string already had it. - return attributedString + + // Determine the effective range of attributes at the insertion point + var effectiveRange = NSRange(location: NSNotFound, length: 0) + let insertionPoint = range.location > 0 ? range.location - 1 : 0 + let currentAttrs = attributes(at: insertionPoint, effectiveRange: &effectiveRange) + + // Apply 'markHtmlRepresentation' attribute if present + if let markSize = currentAttrs[.markHtmlRepresentation] { + let processedString = NSMutableAttributedString(attributedString: attributedString) + processedString.addAttribute(.markHtmlRepresentation, value: markSize, range: NSRange(location: 0, length: attributedString.length)) + return processedString } - - let processedString = NSMutableAttributedString(attributedString: attributedString) - processedString.addAttribute(.markHtmlRepresentation, value: markSize, range: attributedString.rangeOfEntireString) - - return processedString + return attributedString } fileprivate func detectAttachmentRemoved(in range: NSRange) { @@ -363,9 +353,13 @@ open class TextStorage: NSTextStorage { override open func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) { beginEditing() + // Ensure matching styles for the font and paragraph headers let fixedAttributes = ensureMatchingFontAndParagraphHeaderStyles(beforeApplying: attrs ?? [:], at: range) - textStore.setAttributes(fixedAttributes, range: range) + // Adjust attributes for 'mark' formatting logic + let adjustedAttributes = adjustAttributesForMark(fixedAttributes, range: range) + + textStore.setAttributes(adjustedAttributes, range: range) edited(.editedAttributes, range: range, changeInLength: 0) endEditing() @@ -523,6 +517,34 @@ private extension TextStorage { } } +// MARK: - Mark Formatting Attribute Fixes +// +private extension TextStorage { + /// Adjusts text attributes to preserve the color of text marked with 'markHtmlRepresentation'. + /// + /// This method checks if the specified range of text has the 'markHtmlRepresentation' attribute. + /// If it does, the method retains the existing color attribute to preserve the 'mark' formatting. + /// + /// - Parameters: + /// - attrs: NSAttributedString attributes that are about to be applied. + /// - range: Range of the text being modified. + /// + /// - Returns: Adjusted collection of attributes, preserving color for 'mark' formatted text. + /// + private func adjustAttributesForMark(_ attrs: [NSAttributedString.Key: Any], range: NSRange) -> [NSAttributedString.Key: Any] { + var adjustedAttributes = attrs + + // Check if the range has the 'markHtmlRepresentation' attribute + let hasMarkAttribute = attribute(.markHtmlRepresentation, at: range.location, effectiveRange: nil) != nil + + // If the 'markHtmlRepresentation' attribute is present, retain the existing color + if hasMarkAttribute, let existingColor = textStore.attribute(.foregroundColor, at: range.location, effectiveRange: nil) as? UIColor { + adjustedAttributes[.foregroundColor] = existingColor + } + + return adjustedAttributes + } +} // MARK: - TextStorage: MediaAttachmentDelegate Methods // From fadc31252f74cd0d8e131e17266794bce964f373 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 8 Jan 2024 17:47:57 +0100 Subject: [PATCH 05/13] Fix whitespace issues --- Aztec/Classes/TextKit/TextStorage.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index 867a769f1..13d1bb1df 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -361,7 +361,7 @@ open class TextStorage: NSTextStorage { textStore.setAttributes(adjustedAttributes, range: range) edited(.editedAttributes, range: range, changeInLength: 0) - + endEditing() } @@ -533,15 +533,15 @@ private extension TextStorage { /// private func adjustAttributesForMark(_ attrs: [NSAttributedString.Key: Any], range: NSRange) -> [NSAttributedString.Key: Any] { var adjustedAttributes = attrs - + // Check if the range has the 'markHtmlRepresentation' attribute let hasMarkAttribute = attribute(.markHtmlRepresentation, at: range.location, effectiveRange: nil) != nil - + // If the 'markHtmlRepresentation' attribute is present, retain the existing color if hasMarkAttribute, let existingColor = textStore.attribute(.foregroundColor, at: range.location, effectiveRange: nil) as? UIColor { adjustedAttributes[.foregroundColor] = existingColor } - + return adjustedAttributes } } From 4711da4fe44e0932ea13e0204d16890ec15cfaef Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 12 Jan 2024 19:08:50 +0100 Subject: [PATCH 06/13] Update MarkFormatter to support incoming color changes --- .../Implementations/MarkFormatter.swift | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index 0fe302846..44e0735cb 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -4,25 +4,41 @@ import UIKit class MarkFormatter: AttributeFormatter { var placeholderAttributes: [NSAttributedString.Key: Any]? - + var textColor: String? + var defaulTextColor: UIColor? + func applicationRange(for range: NSRange, in text: NSAttributedString) -> NSRange { return range } func apply(to attributes: [NSAttributedString.Key: Any], andStore representation: HTMLRepresentation?) -> [NSAttributedString.Key: Any] { - var resultingAttributes = attributes + var resultingAttributes = attributes + let colorStyle = CSSAttribute(name: "color", value: textColor) + let backgroundColorStyle = CSSAttribute(name: "background-color", value: "rgba(0, 0, 0, 0)") + + let styleAttribute = Attribute(type: .style, value: .inlineCss([backgroundColorStyle, colorStyle])) + let classAttribute = Attribute(type: .class, value: .string("has-inline-color")) - var representationToUse = HTMLRepresentation(for: .element(HTMLElementRepresentation.init(name: "mark", attributes: []))) + // Setting the HTML representation + var representationToUse = HTMLRepresentation(for: .element(HTMLElementRepresentation.init(name: "mark", attributes: [styleAttribute, classAttribute]))) if let requestedRepresentation = representation { representationToUse = requestedRepresentation } resultingAttributes[.markHtmlRepresentation] = representationToUse + + if (textColor != nil) { + resultingAttributes[.foregroundColor] = UIColor(hexString: textColor!) + } return resultingAttributes } func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] { var resultingAttributes = attributes + + if (defaulTextColor != nil) { + resultingAttributes[.foregroundColor] = defaulTextColor + } resultingAttributes.removeValue(forKey: .markHtmlRepresentation) return resultingAttributes From 590b6461b9e20f406ff3af59574f9372667f5b61 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 12 Jan 2024 19:09:22 +0100 Subject: [PATCH 07/13] Update how toggleMark works to support color customization and resetting the format --- Aztec/Classes/TextKit/TextView.swift | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index d214a0893..4e5b3f416 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -207,6 +207,7 @@ open class TextView: UITextView { HeaderFormatter(headerLevel: .h6), FigureFormatter(), FigcaptionFormatter(), + MarkFormatter(), ] /// At some point moving ahead, this could be dynamically generated from the full list of registered formatters @@ -1154,12 +1155,27 @@ open class TextView: UITextView { /// /// - Parameter range: The NSRange to edit. /// - open func toggleMark(range: NSRange) { + open func toggleMark(range: NSRange, color: String?, resetColor: Bool) { let formatter = MarkFormatter() formatter.placeholderAttributes = self.defaultAttributes - toggle(formatter: formatter, atRange: range) + formatter.defaulTextColor = self.defaultTextColor + formatter.textColor = color - formattingDelegate?.textViewCommandToggledAStyle() + // If the format exists remove the current formatting + // this can happen when the color changed. + if (formatter.present(in: typingAttributes)) { + typingAttributes = formatter.remove(from: typingAttributes) + let applicationRange = formatter.applicationRange(for: selectedRange, in: storage) + formatter.removeAttributes(from: storage, at: applicationRange) + typingAttributes = formatter.remove(from:typingAttributes) + + // Reflect color changes by enabling the formatting again. + if (!resetColor) { + toggle(formatter: formatter, atRange: range) + } + return + } + toggle(formatter: formatter, atRange: range) } /// Replaces with an horizontal ruler on the specified range From 42485a778fe1966c9e0950b5e0e2e8b796f4eed0 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 12 Jan 2024 19:10:02 +0100 Subject: [PATCH 08/13] Update TextStorage to process the mark formatting within replaceCharacters --- Aztec/Classes/TextKit/TextStorage.swift | 50 ++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Aztec/Classes/TextKit/TextStorage.swift b/Aztec/Classes/TextKit/TextStorage.swift index 13d1bb1df..572552cbb 100644 --- a/Aztec/Classes/TextKit/TextStorage.swift +++ b/Aztec/Classes/TextKit/TextStorage.swift @@ -146,9 +146,8 @@ open class TextStorage: NSTextStorage { private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString { let stringWithAttachments = preprocessAttachmentsForInsertion(attributedString) let stringWithHeadings = preprocessHeadingsForInsertion(stringWithAttachments) - let preprocessedString = preprocessMarkForInsertion(stringWithHeadings, range) - return preprocessedString + return stringWithHeadings } /// Preprocesses an attributed string's attachments for insertion in the storage. @@ -254,34 +253,33 @@ open class TextStorage: NSTextStorage { return processedString } - /// Preprocesses an attributed string that is missing a `markHtmlRepresentation` attribute for insertion in the storage, this reuses the same behavior as preprocessHeadingsForInsertion + /// Preprocesses an attributed string that is missing a `markHtmlRepresentation` attribute for insertion in the storage. + /// This method ensures that the `markHtmlRepresentation` attribute, if present in the current text storage, + /// is applied to the new attributed string being inserted. This is particularly useful for maintaining + /// mark formatting in scenarios like autocorrection or predictive text input. /// - /// - Important: This method adds the `markHtmlRepresentation` 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. + /// - Important: This method adds the `markHtmlRepresentation` attribute to the new string if it's determined + /// that the string should contain it, based on existing attributes in the text storage. + /// This helps to overcome issues where autocorrected text does not carry over the `markHtmlRepresentation` attribute. /// /// - Parameters: - /// - attributedString: the string we need to preprocess. + /// - attributedString: The new string to be inserted. + /// - range: The range in the current text storage where the new string is to be inserted. This is used to determine + /// if `markHtmlRepresentation` should be applied to the new string. /// - /// - Returns: the preprocessed string. + /// - Returns: The preprocessed attributed string with `markHtmlRepresentation` applied if necessary. /// fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString { - // If the attributed string is empty, return as is - if attributedString.length == 0 { - return attributedString - } - - // Determine the effective range of attributes at the insertion point - var effectiveRange = NSRange(location: NSNotFound, length: 0) - let insertionPoint = range.location > 0 ? range.location - 1 : 0 - let currentAttrs = attributes(at: insertionPoint, effectiveRange: &effectiveRange) - - // Apply 'markHtmlRepresentation' attribute if present - if let markSize = currentAttrs[.markHtmlRepresentation] { - let processedString = NSMutableAttributedString(attributedString: attributedString) - processedString.addAttribute(.markHtmlRepresentation, value: markSize, range: NSRange(location: 0, length: attributedString.length)) - return processedString + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) + + if range.location < textStore.length && range.length > 0 { + let currentAttrs = textStore.attributes(at: range.location, effectiveRange: nil) + + if let markAttribute = currentAttrs[.markHtmlRepresentation] { + mutableAttributedString.addAttribute(.markHtmlRepresentation, value: markAttribute, range: NSRange(location: 0, length: mutableAttributedString.length)) + } } - return attributedString + return mutableAttributedString } fileprivate func detectAttachmentRemoved(in range: NSRange) { @@ -335,14 +333,16 @@ open class TextStorage: NSTextStorage { } override open func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) { - let preprocessedString = preprocessAttributesForInsertion(attrString, range) beginEditing() detectAttachmentRemoved(in: range) - textStore.replaceCharacters(in: range, with: preprocessedString) + // Apply mark formatting to the replacement string + let markFormattedString = preprocessMarkForInsertion(preprocessedString, range) + + textStore.replaceCharacters(in: range, with: markFormattedString) replaceTextStoreString(range, with: attrString.string) edited([.editedAttributes, .editedCharacters], range: range, changeInLength: attrString.length - range.length) From 2970e7f70d7cbe9a2ceb7dd1a71754fe72ae8fb6 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 19 Jan 2024 14:40:58 +0100 Subject: [PATCH 09/13] Fix Hound lint issues --- .../Formatters/Implementations/MarkFormatter.swift | 10 +++++----- Aztec/Classes/TextKit/TextView.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index 44e0735cb..f7787203a 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -6,7 +6,7 @@ class MarkFormatter: AttributeFormatter { var placeholderAttributes: [NSAttributedString.Key: Any]? var textColor: String? var defaulTextColor: UIColor? - + func applicationRange(for range: NSRange, in text: NSAttributedString) -> NSRange { return range } @@ -25,8 +25,8 @@ class MarkFormatter: AttributeFormatter { representationToUse = requestedRepresentation } resultingAttributes[.markHtmlRepresentation] = representationToUse - - if (textColor != nil) { + + if textColor != nil { resultingAttributes[.foregroundColor] = UIColor(hexString: textColor!) } @@ -35,8 +35,8 @@ class MarkFormatter: AttributeFormatter { func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] { var resultingAttributes = attributes - - if (defaulTextColor != nil) { + + if defaulTextColor != nil { resultingAttributes[.foregroundColor] = defaulTextColor } diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index 4e5b3f416..8e3180720 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -1163,14 +1163,14 @@ open class TextView: UITextView { // If the format exists remove the current formatting // this can happen when the color changed. - if (formatter.present(in: typingAttributes)) { + if formatter.present(in: typingAttributes) { typingAttributes = formatter.remove(from: typingAttributes) let applicationRange = formatter.applicationRange(for: selectedRange, in: storage) formatter.removeAttributes(from: storage, at: applicationRange) - typingAttributes = formatter.remove(from:typingAttributes) - + typingAttributes = formatter.remove(from: typingAttributes) + // Reflect color changes by enabling the formatting again. - if (!resetColor) { + if !resetColor { toggle(formatter: formatter, atRange: range) } return From 87123879b35a681eca6e32187e06eefae36a9ec9 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Fri, 19 Jan 2024 14:42:49 +0100 Subject: [PATCH 10/13] Fix lint issue --- .../Implementations/MarkStringAttributeConverter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift index 14be4b5bc..c1a7c4fc4 100644 --- a/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift +++ b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift @@ -1,7 +1,6 @@ import Foundation import UIKit - /// Converts the mark style information from string attributes and aggregates it into an /// existing array of element nodes. /// From a41d881f6a831fecef9e6400e69a64aafe80d9ac Mon Sep 17 00:00:00 2001 From: Gerardo Date: Tue, 23 Jan 2024 18:38:24 +0100 Subject: [PATCH 11/13] Update MarkConverter to remove extra spaces within the style attribute --- .../Implementations/MarkStringAttributeConverter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift index c1a7c4fc4..cf45c4bbb 100644 --- a/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift +++ b/Aztec/Classes/Converters/StringAttributesToAttributes/Implementations/MarkStringAttributeConverter.swift @@ -19,6 +19,12 @@ open class MarkStringAttributeConverter: StringAttributeConverter { // if let elementNode = attributes.storedElement(for: NSAttributedString.Key.markHtmlRepresentation) { + let styleAttribute = elementNode.attributes.first(where: { $0.name == "style" }) + if let elementStyle = styleAttribute?.value.toString() { + // Remove spaces between attribute name and value, and between style attributes. + let styleAttributes = elementStyle.replacingOccurrences(of: ": ", with: ":").replacingOccurrences(of: "; ", with: ";") + elementNode.attributes["style"] = .string(styleAttributes) + } elementNodes.append(elementNode) } From 28e62ab8f254eec55b87e4a5607e2c9d4a9887d4 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 18 Mar 2024 13:08:00 +0100 Subject: [PATCH 12/13] Use if let to unwrap textColor value --- Aztec/Classes/Formatters/Implementations/MarkFormatter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index f7787203a..652783e4e 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -26,8 +26,8 @@ class MarkFormatter: AttributeFormatter { } resultingAttributes[.markHtmlRepresentation] = representationToUse - if textColor != nil { - resultingAttributes[.foregroundColor] = UIColor(hexString: textColor!) + if let textColor = textColor { + resultingAttributes[.foregroundColor] = UIColor(hexString: textColor) } return resultingAttributes From 7b9cc634d514835a0fd92d7743123211b06da4ee Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 18 Mar 2024 13:11:48 +0100 Subject: [PATCH 13/13] Fix defaultTextColor typo --- .../Classes/Formatters/Implementations/MarkFormatter.swift | 6 +++--- Aztec/Classes/TextKit/TextView.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift index 652783e4e..e180689e8 100644 --- a/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift +++ b/Aztec/Classes/Formatters/Implementations/MarkFormatter.swift @@ -5,7 +5,7 @@ class MarkFormatter: AttributeFormatter { var placeholderAttributes: [NSAttributedString.Key: Any]? var textColor: String? - var defaulTextColor: UIColor? + var defaultTextColor: UIColor? func applicationRange(for range: NSRange, in text: NSAttributedString) -> NSRange { return range @@ -36,8 +36,8 @@ class MarkFormatter: AttributeFormatter { func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] { var resultingAttributes = attributes - if defaulTextColor != nil { - resultingAttributes[.foregroundColor] = defaulTextColor + if defaultTextColor != nil { + resultingAttributes[.foregroundColor] = defaultTextColor } resultingAttributes.removeValue(forKey: .markHtmlRepresentation) diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index 8e3180720..7656a6831 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -1158,7 +1158,7 @@ open class TextView: UITextView { open func toggleMark(range: NSRange, color: String?, resetColor: Bool) { let formatter = MarkFormatter() formatter.placeholderAttributes = self.defaultAttributes - formatter.defaulTextColor = self.defaultTextColor + formatter.defaultTextColor = self.defaultTextColor formatter.textColor = color // If the format exists remove the current formatting