Skip to content

Commit

Permalink
Mark Formatting - Fix autocorrected strings and changing the caret po…
Browse files Browse the repository at this point in the history
…sition bug
  • Loading branch information
Gerardo committed Mar 14, 2022
1 parent b16c763 commit d655451
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Aztec.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -288,6 +289,7 @@
40A298701FD61B6F00AEDF3B /* ImageElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageElementConverter.swift; sourceTree = "<group>"; };
40A298721FD61E1900AEDF3B /* VideoElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoElementConverter.swift; sourceTree = "<group>"; };
50A1CC6E250FEA93001D5517 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkStringAttributeConverter.swift; sourceTree = "<group>"; };
568FF25727552BFF0057B2E3 /* MarkFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkFormatter.swift; sourceTree = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -1101,6 +1103,7 @@
F15BA60C215159A600424120 /* ItalicStringAttributeConverter.swift */,
FF94935D245738AC0085ABB3 /* SuperscriptStringAttributeConverter.swift */,
FF949361245744090085ABB3 /* SubscriptStringAttributeConverter.swift */,
5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */,
F15BA60E21515C0F00424120 /* UnderlineStringAttributeConverter.swift */,
);
path = Implementations;
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -60,6 +61,7 @@ class GenericElementConverter: ElementConverter {
.li: self.liFormatter,
.sup: self.superscriptFormatter,
.sub: self.subscriptFormatter,
.mark: self.markFormatter,
]
}()

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class AttributedStringParser {
UnderlineStringAttributeConverter(),
SuperscriptStringAttributeConverter(),
SubscriptStringAttributeConverter(),
MarkStringAttributeConverter(),
]

// MARK: - Attachment Converters
Expand Down
38 changes: 37 additions & 1 deletion Aztec/Classes/TextKit/TextStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions Aztec/Classes/TextKit/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions AztecTests/TextKit/TextStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit d655451

Please sign in to comment.