Skip to content

Commit d655451

Browse files
author
Gerardo
committed
Mark Formatting - Fix autocorrected strings and changing the caret position bug
1 parent b16c763 commit d655451

8 files changed

Lines changed: 116 additions & 2 deletions

File tree

Aztec.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
40A2986D1FD61B0C00AEDF3B /* ElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A2986C1FD61B0C00AEDF3B /* ElementConverter.swift */; };
1414
40A298711FD61B6F00AEDF3B /* ImageElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A298701FD61B6F00AEDF3B /* ImageElementConverter.swift */; };
1515
40A298731FD61E1900AEDF3B /* VideoElementConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A298721FD61E1900AEDF3B /* VideoElementConverter.swift */; };
16+
5608841E27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */; };
1617
568FF25827552BFF0057B2E3 /* MarkFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568FF25727552BFF0057B2E3 /* MarkFormatter.swift */; };
1718
594C9D6F1D8BE61F00D74542 /* Aztec.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5951CB8E1D8BC93600E1866F /* Aztec.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1819
594C9D731D8BE6C300D74542 /* InAttributeConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FEA06B1D8BDFA700D138DF /* InAttributeConverterTests.swift */; };
@@ -288,6 +289,7 @@
288289
40A298701FD61B6F00AEDF3B /* ImageElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageElementConverter.swift; sourceTree = "<group>"; };
289290
40A298721FD61E1900AEDF3B /* VideoElementConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoElementConverter.swift; sourceTree = "<group>"; };
290291
50A1CC6E250FEA93001D5517 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = "<group>"; };
292+
5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkStringAttributeConverter.swift; sourceTree = "<group>"; };
291293
568FF25727552BFF0057B2E3 /* MarkFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkFormatter.swift; sourceTree = "<group>"; };
292294
5951CB8E1D8BC93600E1866F /* Aztec.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Aztec.framework; sourceTree = BUILT_PRODUCTS_DIR; };
293295
5951CB921D8BC93600E1866F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -1101,6 +1103,7 @@
11011103
F15BA60C215159A600424120 /* ItalicStringAttributeConverter.swift */,
11021104
FF94935D245738AC0085ABB3 /* SuperscriptStringAttributeConverter.swift */,
11031105
FF949361245744090085ABB3 /* SubscriptStringAttributeConverter.swift */,
1106+
5608841D27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift */,
11041107
F15BA60E21515C0F00424120 /* UnderlineStringAttributeConverter.swift */,
11051108
);
11061109
path = Implementations;
@@ -1547,6 +1550,7 @@
15471550
F1584794203C94AC00EE05A1 /* Dictionary+AttributedStringKey.swift in Sources */,
15481551
B572AC281E817CFE008948C2 /* CommentAttachment.swift in Sources */,
15491552
F1E1D5881FEC52EE0086B339 /* GenericElementConverter.swift in Sources */,
1553+
5608841E27DBA33600DA8AA7 /* MarkStringAttributeConverter.swift in Sources */,
15501554
F1FA0E861E6EF514009D98EE /* Node.swift in Sources */,
15511555
FFD3C1712344DB4E00AE8DA0 /* ForegroundColorCSSAttributeMatcher.swift in Sources */,
15521556
FFD3C1732344DCA900AE8DA0 /* ForegroundColorElementAttributeConverter.swift in Sources */,

Aztec/Classes/Converters/ElementsToAttributedString/Implementations/GenericElementConverter.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class GenericElementConverter: ElementConverter {
3636
lazy var liFormatter = LiFormatter()
3737
lazy var superscriptFormatter = SuperscriptFormatter()
3838
lazy var subscriptFormatter = SubscriptFormatter()
39+
lazy var markFormatter = MarkFormatter()
3940

4041
public lazy var elementFormattersMap: [Element: AttributeFormatter] = {
4142
return [
@@ -60,6 +61,7 @@ class GenericElementConverter: ElementConverter {
6061
.li: self.liFormatter,
6162
.sup: self.superscriptFormatter,
6263
.sub: self.subscriptFormatter,
64+
.mark: self.markFormatter,
6365
]
6466
}()
6567

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Foundation
2+
import UIKit
3+
4+
5+
/// Converts the mark style information from string attributes and aggregates it into an
6+
/// existing array of element nodes.
7+
///
8+
open class MarkStringAttributeConverter: StringAttributeConverter {
9+
10+
private let toggler = HTMLStyleToggler(defaultElement: .mark, cssAttributeMatcher: ForegroundColorCSSAttributeMatcher())
11+
12+
public func convert(
13+
attributes: [NSAttributedString.Key: Any],
14+
andAggregateWith elementNodes: [ElementNode]) -> [ElementNode] {
15+
16+
var elementNodes = elementNodes
17+
18+
// We add the representation right away, if it exists... as it could contain attributes beyond just this
19+
// style. The enable and disable methods below can modify this as necessary.
20+
//
21+
22+
if let elementNode = attributes.storedElement(for: NSAttributedString.Key.markHtmlRepresentation) {
23+
elementNodes.append(elementNode)
24+
}
25+
26+
if shouldEnableMarkElement(for: attributes) {
27+
return toggler.enable(in: elementNodes)
28+
} else {
29+
return toggler.disable(in: elementNodes)
30+
}
31+
}
32+
33+
// MARK: - Style Detection
34+
35+
func shouldEnableMarkElement(for attributes: [NSAttributedString.Key: Any]) -> Bool {
36+
return isMark(for: attributes)
37+
}
38+
39+
func isMark(for attributes: [NSAttributedString.Key: Any]) -> Bool {
40+
return attributes[.markHtmlRepresentation] != nil
41+
}
42+
}

Aztec/Classes/Formatters/Implementations/MarkFormatter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class MarkFormatter: AttributeFormatter {
2424
func remove(from attributes: [NSAttributedString.Key: Any]) -> [NSAttributedString.Key: Any] {
2525
var resultingAttributes = attributes
2626

27+
resultingAttributes[.foregroundColor] = placeholderAttributes![.foregroundColor]
2728
resultingAttributes.removeValue(forKey: .markHtmlRepresentation)
28-
2929
return resultingAttributes
3030
}
3131

Aztec/Classes/NSAttributedString/Conversions/AttributedStringParser.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class AttributedStringParser {
2929
UnderlineStringAttributeConverter(),
3030
SuperscriptStringAttributeConverter(),
3131
SubscriptStringAttributeConverter(),
32+
MarkStringAttributeConverter(),
3233
]
3334

3435
// MARK: - Attachment Converters

Aztec/Classes/TextKit/TextStorage.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ open class TextStorage: NSTextStorage {
145145

146146
private func preprocessAttributesForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString {
147147
let stringWithAttachments = preprocessAttachmentsForInsertion(attributedString)
148-
let preprocessedString = preprocessHeadingsForInsertion(stringWithAttachments)
148+
let stringWithHeadings = preprocessHeadingsForInsertion(stringWithAttachments)
149+
let preprocessedString = preprocessMarkForInsertion(stringWithHeadings)
149150

150151
return preprocessedString
151152
}
@@ -253,6 +254,41 @@ open class TextStorage: NSTextStorage {
253254
return processedString
254255
}
255256

257+
/// Preprocesses an attributed string that is missing a `markHtmlRepresentation` attribute for insertion in the storage, this reuses the same behavior as preprocessHeadingsForInsertion
258+
///
259+
/// - Important: This method adds the `markHtmlRepresentation` attribute if it determines the string should contain it.
260+
/// This works around a problem where autocorrected text didn't contain the attribute. This may change in future versions.
261+
///
262+
/// - Parameters:
263+
/// - attributedString: the string we need to preprocess.
264+
///
265+
/// - Returns: the preprocessed string.
266+
///
267+
fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString) -> NSAttributedString {
268+
guard textStore.length > 0, attributedString.length > 0 else {
269+
return attributedString
270+
}
271+
272+
// Get the attributes of the start of the current string in storage.
273+
let currentAttrs = attributes(at: 0, effectiveRange: nil)
274+
275+
guard
276+
// the text currently in storage has a markHtmlRepresentation key
277+
let markSize = currentAttrs[.markHtmlRepresentation],
278+
// the text coming in doesn't have a markHtmlRepresentation key
279+
attributedString.attribute(.markHtmlRepresentation, at: 0, effectiveRange: nil) == nil
280+
else {
281+
// Either the mark attribute wasn't present in the existing string,
282+
// or the attributed string already had it.
283+
return attributedString
284+
}
285+
286+
let processedString = NSMutableAttributedString(attributedString: attributedString)
287+
processedString.addAttribute(.markHtmlRepresentation, value: markSize, range: attributedString.rangeOfEntireString)
288+
289+
return processedString
290+
}
291+
256292
fileprivate func detectAttachmentRemoved(in range: NSRange) {
257293
// Ref. https://github.com/wordpress-mobile/AztecEditor-iOS/issues/727:
258294
// If the delegate is not set, we *Explicitly* do not want to crash here.

Aztec/Classes/TextKit/TextView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,8 @@ open class TextView: UITextView {
11601160
let formatter = MarkFormatter()
11611161
formatter.placeholderAttributes = self.defaultAttributes
11621162
toggle(formatter: formatter, atRange: range)
1163+
1164+
formattingDelegate?.textViewCommandToggledAStyle()
11631165
}
11641166

11651167
/// Replaces with an horizontal ruler on the specified range

AztecTests/TextKit/TextStorageTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,4 +633,31 @@ class TextStorageTests: XCTestCase {
633633
XCTAssertEqual(storage.string, "Hello I'm a paragraph")
634634
XCTAssertNil(finalAttributes[.headingRepresentation])
635635
}
636+
637+
/// Verifies that missing Mark attributes are retained on string replacements when appropriate
638+
///
639+
func testMissingMarkAttributeIsRetained() {
640+
let formatter = MarkFormatter()
641+
storage.replaceCharacters(in: storage.rangeOfEntireString, with: "Hello i'm a text highlighted")
642+
formatter.applyAttributes(to: storage, at: storage.rangeOfEntireString)
643+
644+
let originalAttributes = storage.attributes(at: 0, effectiveRange: nil)
645+
XCTAssertEqual(storage.string, "Hello i'm a text highlighted")
646+
XCTAssertEqual(originalAttributes.count, 2)
647+
XCTAssertNotNil(originalAttributes[.markHtmlRepresentation])
648+
649+
let autoCorrectedAttributes = originalAttributes.filter { $0.key != .markHtmlRepresentation }
650+
651+
let autoCorrectedString = NSAttributedString(
652+
string: "I'm",
653+
attributes: autoCorrectedAttributes
654+
)
655+
656+
let range = NSRange(location: 6, length: 3)
657+
storage.replaceCharacters(in: range, with: autoCorrectedString)
658+
659+
let finalAttributes = storage.attributes(at: range.location, effectiveRange: nil)
660+
XCTAssertEqual(storage.string, "Hello I'm a text highlighted")
661+
XCTAssertEqual(originalAttributes.keys, finalAttributes.keys)
662+
}
636663
}

0 commit comments

Comments
 (0)