Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Mark Formatting - Fix autocorrected strings and changing the caret po…
…sition bug
  • Loading branch information
Gerardo committed Mar 14, 2022
commit d6554516c81f08184ddffc0c51cb4b03eb70fa3c
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)
}
}