diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift index 7a5e8732b..03015f86d 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift @@ -35,4 +35,70 @@ extension DTHTMLElement { } } } + + func clearTrailingAndLeadingNewlinesInCodeblocks() { + guard let childNodes = childNodes as? [DTHTMLElement] else { + return + } + + if name == "pre", + childNodes.count == 1, + let child = childNodes.first as? DTTextHTMLElement, + var text = child.text(), + text != .nbsp { + var leadingDiscardableElement: DiscardableTextHTMLElement? + var trailingDiscardableElement: DiscardableTextHTMLElement? + var shouldReplaceNodes = false + + if text.hasPrefix("\(Character.nbsp)") { + shouldReplaceNodes = true + text.removeFirst() + leadingDiscardableElement = createDiscardableElement() + } + + if text.hasSuffix("\(Character.nbsp)") { + shouldReplaceNodes = true + text.removeLast() + trailingDiscardableElement = createDiscardableElement() + } + + if shouldReplaceNodes { + removeAllChildNodes() + + if let leadingDiscardableElement = leadingDiscardableElement { + addChildNode(leadingDiscardableElement) + addChildNode(createLineBreak()) + } + + let newTextNode = DTTextHTMLElement() + newTextNode.inheritAttributes(from: self) + newTextNode.interpretAttributes() + newTextNode.setText(text) + addChildNode(newTextNode) + + if let trailingDiscardableElement = trailingDiscardableElement { + addChildNode(createLineBreak()) + addChildNode(trailingDiscardableElement) + } + } + } else { + for childNode in childNodes { + childNode.clearTrailingAndLeadingNewlinesInCodeblocks() + } + } + } + + private func createDiscardableElement() -> DiscardableTextHTMLElement { + let discardableElement = DiscardableTextHTMLElement() + discardableElement.inheritAttributes(from: self) + discardableElement.interpretAttributes() + return discardableElement + } + + private func createLineBreak() -> DTBreakHTMLElement { + let lineBreakElement = DTBreakHTMLElement() + lineBreakElement.inheritAttributes(from: self) + lineBreakElement.interpretAttributes() + return lineBreakElement + } } diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DiscardableTextHTMLElement.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DiscardableTextHTMLElement.swift index d853aee0c..3d40eb02d 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DiscardableTextHTMLElement.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DiscardableTextHTMLElement.swift @@ -26,6 +26,11 @@ final class DiscardableTextHTMLElement: DTTextHTMLElement { setText(textNode.text()) } + override init() { + super.init() + setText(.nbsp) + } + override func attributesForAttributedStringRepresentation() -> [AnyHashable: Any]! { var dict = super.attributesForAttributedStringRepresentation() ?? [AnyHashable: Any]() // Insert a key to mark this as discardable post-parsing. diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSAttributedString+Range.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSAttributedString+Range.swift index 41bf35bd1..4ccd30828 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSAttributedString+Range.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSAttributedString+Range.swift @@ -83,6 +83,20 @@ extension NSAttributedString { return ranges } + func discardableTextRanges(in range: NSRange? = nil) -> [NSRange] { + let enumRange = range ?? .init(location: 0, length: length) + var ranges = [NSRange]() + + enumerateAttribute(.discardableText, + in: enumRange) { (attr: Any?, range: NSRange, _) in + if attr != nil { + ranges.append(range) + } + } + + return ranges + } + /// Computes index inside the HTML raw text from the index /// inside the attributed representation. /// @@ -95,9 +109,17 @@ extension NSAttributedString { .outOfBoundsAttributedIndex(index: attributedIndex) } + let discardableTextRanges = discardableTextRanges() + var actualIndex = attributedIndex + + for discardableTextRange in discardableTextRanges { + if discardableTextRange.upperBound <= attributedIndex { + actualIndex -= discardableTextRange.length + } + } + let prefixes = listPrefixesRanges() - var actualIndex: Int = attributedIndex - + for listPrefix in prefixes { if listPrefix.upperBound <= attributedIndex { actualIndex -= listPrefix.length @@ -117,8 +139,16 @@ extension NSAttributedString { /// - htmlIndex: the index inside the HTML raw text /// - Returns: the index inside the attributed representation func attributedPosition(at htmlIndex: Int) throws -> Int { + let discardableTextRanges = discardableTextRanges() + var actualIndex = htmlIndex + + for discardableTextRange in discardableTextRanges { + if discardableTextRange.location < actualIndex { + actualIndex += discardableTextRange.length + } + } + let prefixes = listPrefixesRanges() - var actualIndex: Int = htmlIndex for listPrefix in prefixes { if listPrefix.location < actualIndex { diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift index 6acabd6cc..715a23f8b 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift @@ -53,12 +53,18 @@ extension NSMutableAttributedString { } } - /// Removes any text that has been marked as discardable. - func removeDiscardableText() { + /// Finds any text that has been marked as discardable + /// and either replaces it with ZWSP if contained overlaps with text marked with a background style + /// or removes it otherwise + func replaceOrDeleteDiscardableText() { enumerateTypedAttribute(.discardableText) { (discardable: Bool, range: NSRange, _) in guard discardable == true else { return } - - self.deleteCharacters(in: range) + let attributes = self.attributes(at: range.location, effectiveRange: nil) + if attributes[.backgroundStyle] != nil { + self.replaceCharacters(in: range, with: String.zwsp) + } else { + self.deleteCharacters(in: range) + } } } diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/String+Character.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/String+Character.swift index 3db517934..f0b3205a1 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/String+Character.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/String+Character.swift @@ -16,8 +16,10 @@ extension String { static let nbsp = "\(Character.nbsp)" + static let zwsp = "\(Character.zwsp)" } public extension Character { static let nbsp = Character("\u{00A0}") + static let zwsp = Character("\u{200B}") } diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index a653d96e5..1e035dec9 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -88,6 +88,7 @@ public final class HTMLParser { // Removing NBSP character from
since it is only used to // make DTCoreText able to easily parse new lines. element.clearNbspNodes() + element.clearTrailingAndLeadingNewlinesInCodeblocks() } guard let attributedString = builder.generatedAttributedString() else { @@ -114,7 +115,7 @@ public final class HTMLParser { mutableAttributedString.applyBackgroundStyles(style: style) mutableAttributedString.applyInlineCodeBackgroundStyle(codeBackgroundColor: style.codeBackgroundColor) - mutableAttributedString.removeDiscardableText() + mutableAttributedString.replaceOrDeleteDiscardableText() mutableAttributedString.removeParagraphVerticalSpacing() removeTrailingNewlineIfNeeded(from: mutableAttributedString, given: html) @@ -124,14 +125,17 @@ public final class HTMLParser { private static func removeTrailingNewlineIfNeeded(from mutableAttributedString: NSMutableAttributedString, given html: String) { // DTCoreText always adds a \n at the end of the document, which we need to remove // however it does not add it if are the last nodes. - // Also we don't want to remove it if a codeblock contains that newline - // and is not empty, because DTCoreText does not add a newline if these blocks - // contain one at the end. + // It should give also issues with codeblock and blockquote when they contain newlines + // but the usage of nbsp and zwsp solves that if mutableAttributedString.string.last == "\n", !html.hasSuffix(""), - !html.hasSuffix(""), - !html.hasSuffix("\n") { - mutableAttributedString.deleteCharacters(in: NSRange(location: mutableAttributedString.length - 1, length: 1)) + !html.hasSuffix("") { + mutableAttributedString.deleteCharacters( + in: NSRange( + location: mutableAttributedString.length - 1, + length: 1 + ) + ) } } } diff --git a/platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Tools/StringDiffer.swift b/platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Tools/StringDiffer.swift index 81322d56a..efacc9846 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Tools/StringDiffer.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Tools/StringDiffer.swift @@ -117,7 +117,7 @@ private struct StringDiff { private extension String { /// Converts all whitespaces to NBSP to avoid diffs caused by HTML translations. var withNBSP: String { - String(map { $0.isWhitespace ? Character.nbsp : $0 }) + String(map { $0.isWhitespace ? Character.nbsp : $0 }).trimmingCharacters(in: .whitespacesAndNewlines) } /// Computes the diff from provided string to self. Outputs UTF16 locations and lengths.