Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Mark formatting #1352

Merged
merged 14 commits into from Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions Aztec.xcodeproj/project.pbxproj
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
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
@@ -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
}
}
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]
geriux marked this conversation as resolved.
Show resolved Hide resolved
resultingAttributes.removeValue(forKey: .markHtmlRepresentation)

return resultingAttributes
}

Expand Down
Expand Up @@ -29,6 +29,7 @@ class AttributedStringParser {
UnderlineStringAttributeConverter(),
SuperscriptStringAttributeConverter(),
SubscriptStringAttributeConverter(),
MarkStringAttributeConverter(),
]

// MARK: - Attachment Converters
Expand Down
47 changes: 44 additions & 3 deletions Aztec/Classes/TextKit/TextStorage.swift
Expand Up @@ -143,9 +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 preprocessedString = preprocessHeadingsForInsertion(stringWithAttachments)
let stringWithHeadings = preprocessHeadingsForInsertion(stringWithAttachments)
let preprocessedString = preprocessMarkForInsertion(stringWithHeadings, range)

return preprocessedString
}
Expand Down Expand Up @@ -253,6 +254,46 @@ 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.
///
geriux marked this conversation as resolved.
Show resolved Hide resolved
/// - Returns: the preprocessed string.
///
fileprivate func preprocessMarkForInsertion(_ attributedString: NSAttributedString, _ range: NSRange) -> NSAttributedString {
guard textStore.length > 0, attributedString.length > 0, range.endLocation != range.lowerBound else {
geriux marked this conversation as resolved.
Show resolved Hide resolved
return attributedString
}
var startAt = 0

if range.endLocation > range.lowerBound {
geriux marked this conversation as resolved.
Show resolved Hide resolved
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
}

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 Expand Up @@ -305,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()

Expand Down
2 changes: 2 additions & 0 deletions Aztec/Classes/TextKit/TextView.swift
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
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)
}
}