From 67cf4e790da428d998bf24f6c9de8070b1bbf3c0 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 25 Jan 2023 16:24:34 +0100 Subject: [PATCH 1/7] close to the solution but still experiencing a misalignment --- .../Extensions/NSAttributedString+Range.swift | 36 +++++++++++++++++-- .../NSMutableAttributedString.swift | 4 +-- .../Extensions/String+Character.swift | 2 ++ .../Sources/HTMLParser/HTMLParser.swift | 2 +- 4 files changed, 38 insertions(+), 6 deletions(-) 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 f31295f76..8fd0ce345 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift @@ -52,11 +52,11 @@ extension NSMutableAttributedString { } /// Removes any text that has been marked as discardable. - func removeDiscardableText() { + func replaceDiscardableTextWithZwsp() { enumerateTypedAttribute(.discardableText) { (discardable: Bool, range: NSRange, _) in guard discardable == true else { return } - self.deleteCharacters(in: range) + self.replaceCharacters(in: range, with: String.zwsp) } } } 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 6ddc98d2f..800ff4ae1 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -114,7 +114,7 @@ public final class HTMLParser { mutableAttributedString.applyBackgroundStyles(style: style) mutableAttributedString.applyInlineCodeBackgroundStyle(codeBackgroundColor: style.codeBackgroundColor) - mutableAttributedString.removeDiscardableText() + mutableAttributedString.replaceDiscardableTextWithZwsp() // FIXME: This solution might not fit for everything. mutableAttributedString.addAttribute(.paragraphStyle, From dcd024b718552d74e7f5c5dfc14f3d01b679bf4b Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 25 Jan 2023 16:57:04 +0100 Subject: [PATCH 2/7] partial fix for the end of the document --- .../Sources/WysiwygComposer/Tools/StringDiffer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From e2ad771eea6414aad48e65572e11cb9f9925e7be Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 25 Jan 2023 19:04:26 +0100 Subject: [PATCH 3/7] replaceNewlinesWithDiscardableElements however it doesn't work properly yet --- .../Extensions/DTCoreText/DTHTMLElement.swift | 36 +++++++++++++++++++ .../DiscardableTextHTMLElement.swift | 5 +++ .../Sources/HTMLParser/HTMLParser.swift | 1 + 3 files changed, 42 insertions(+) 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..e30537c53 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,40 @@ extension DTHTMLElement { } } } + + func replaceNewlinesWithDiscardableElements() { + guard let childNodes = childNodes as? [DTHTMLElement] else { return } + + if childNodes.count == 1, + let child = childNodes.first as? DTTextHTMLElement { + let splits = child.text().split(separator: "\n", omittingEmptySubsequences: false) + guard splits.count > 1, splits.contains("") else { return } + + let strings: [String] = splits.map { "\($0)\n" } + removeAllChildNodes() + for string in strings { + if string != "\n" { + var textElement = DTTextHTMLElement() + textElement.setText("") + if let lastChild = childNodes.last as? DTTextHTMLElement { + textElement = lastChild + } else { + addChildNode(textElement) + textElement.inheritAttributes(from: self) + textElement.interpretAttributes() + } + textElement.setText(textElement.text() + string) + } else { + let newChild = DiscardableTextHTMLElement() + addChildNode(newChild) + newChild.inheritAttributes(from: self) + newChild.interpretAttributes() + } + } + } else { + for childNode in childNodes { + childNode.replaceNewlinesWithDiscardableElements() + } + } + } } 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/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index 7fabc9906..ce92bf1f5 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.replaceNewlinesWithDiscardableElements() } guard let attributedString = builder.generatedAttributedString() else { From 66e029e0e1e27598e8f9639b9d857ad21eca3069 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 26 Jan 2023 17:22:30 +0100 Subject: [PATCH 4/7] improvements but the replace doesn't always work --- .../Extensions/DTCoreText/DTHTMLElement.swift | 76 ++++++++++++------- .../Sources/HTMLParser/HTMLParser.swift | 11 ++- 2 files changed, 58 insertions(+), 29 deletions(-) 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 e30537c53..846513153 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift @@ -36,39 +36,63 @@ extension DTHTMLElement { } } - func replaceNewlinesWithDiscardableElements() { - guard let childNodes = childNodes as? [DTHTMLElement] else { return } + func clearTrailingAndLeadingNewlinesInCodeblocks() { + guard let childNodes = childNodes as? [DTHTMLElement] else { + return + } - if childNodes.count == 1, - let child = childNodes.first as? DTTextHTMLElement { - let splits = child.text().split(separator: "\n", omittingEmptySubsequences: false) - guard splits.count > 1, splits.contains("") else { return } + if name == "pre", + childNodes.count == 1, + let child = childNodes.first as? DTTextHTMLElement, + var text = child.text() { + var leadingDiscardableElement: DiscardableTextHTMLElement? + var trailingDiscardableElement: DiscardableTextHTMLElement? + var shouldReplaceNodes = false - let strings: [String] = splits.map { "\($0)\n" } - removeAllChildNodes() - for string in strings { - if string != "\n" { - var textElement = DTTextHTMLElement() - textElement.setText("") - if let lastChild = childNodes.last as? DTTextHTMLElement { - textElement = lastChild - } else { - addChildNode(textElement) - textElement.inheritAttributes(from: self) - textElement.interpretAttributes() - } - textElement.setText(textElement.text() + string) - } else { - let newChild = DiscardableTextHTMLElement() - addChildNode(newChild) - newChild.inheritAttributes(from: self) - newChild.interpretAttributes() + 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() + let container = DTHTMLElement() + container.inheritAttributes(from: self) + container.interpretAttributes() + addChildNode(container) + + if let leadingDiscardableElement = leadingDiscardableElement { + container.addChildNode(leadingDiscardableElement) + } + + let newTextNode = DTTextHTMLElement() + newTextNode.inheritAttributes(from: self) + newTextNode.interpretAttributes() + newTextNode.setText(text) + container.addChildNode(newTextNode) + + if let trailingDiscardableElement = trailingDiscardableElement { + container.addChildNode(trailingDiscardableElement) } } } else { for childNode in childNodes { - childNode.replaceNewlinesWithDiscardableElements() + childNode.clearTrailingAndLeadingNewlinesInCodeblocks() } } } + + private func createDiscardableElement() -> DiscardableTextHTMLElement { + let discardableElement = DiscardableTextHTMLElement() + discardableElement.inheritAttributes(from: self) + discardableElement.interpretAttributes() + return discardableElement + } } diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index ce92bf1f5..4c39a9bd4 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -88,7 +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.replaceNewlinesWithDiscardableElements() + element.clearTrailingAndLeadingNewlinesInCodeblocks() } guard let attributedString = builder.generatedAttributedString() else { @@ -131,8 +131,13 @@ public final class HTMLParser { if mutableAttributedString.string.last == "\n", !html.hasSuffix(""), !html.hasSuffix(""), - !html.hasSuffix("\n") { - mutableAttributedString.deleteCharacters(in: NSRange(location: mutableAttributedString.length - 1, length: 1)) + !html.hasSuffix("\n\(Character.nbsp)") { + mutableAttributedString.deleteCharacters( + in: NSRange( + location: mutableAttributedString.length - 1, + length: 1 + ) + ) } } } From 97feeebf076357bb9dd6f17c8518748d1d0edc12 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 26 Jan 2023 19:12:40 +0100 Subject: [PATCH 5/7] finally this works --- .../Extensions/DTCoreText/DTHTMLElement.swift | 22 ++++++++++++------- .../Sources/HTMLParser/HTMLParser.swift | 3 +-- 2 files changed, 15 insertions(+), 10 deletions(-) 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 846513153..03015f86d 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/DTCoreText/DTHTMLElement.swift @@ -44,7 +44,8 @@ extension DTHTMLElement { if name == "pre", childNodes.count == 1, let child = childNodes.first as? DTTextHTMLElement, - var text = child.text() { + var text = child.text(), + text != .nbsp { var leadingDiscardableElement: DiscardableTextHTMLElement? var trailingDiscardableElement: DiscardableTextHTMLElement? var shouldReplaceNodes = false @@ -63,23 +64,21 @@ extension DTHTMLElement { if shouldReplaceNodes { removeAllChildNodes() - let container = DTHTMLElement() - container.inheritAttributes(from: self) - container.interpretAttributes() - addChildNode(container) if let leadingDiscardableElement = leadingDiscardableElement { - container.addChildNode(leadingDiscardableElement) + addChildNode(leadingDiscardableElement) + addChildNode(createLineBreak()) } let newTextNode = DTTextHTMLElement() newTextNode.inheritAttributes(from: self) newTextNode.interpretAttributes() newTextNode.setText(text) - container.addChildNode(newTextNode) + addChildNode(newTextNode) if let trailingDiscardableElement = trailingDiscardableElement { - container.addChildNode(trailingDiscardableElement) + addChildNode(createLineBreak()) + addChildNode(trailingDiscardableElement) } } } else { @@ -95,4 +94,11 @@ extension DTHTMLElement { 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/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index 4c39a9bd4..0c9675a14 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -130,8 +130,7 @@ public final class HTMLParser { // contain one at the end. if mutableAttributedString.string.last == "\n", !html.hasSuffix(""), - !html.hasSuffix(""), - !html.hasSuffix("\n\(Character.nbsp)") { + !html.hasSuffix("") { mutableAttributedString.deleteCharacters( in: NSRange( location: mutableAttributedString.length - 1, From 60102e64f2fb50b0efc72aa39620561467e6e046 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 26 Jan 2023 19:29:52 +0100 Subject: [PATCH 6/7] comment improvement --- .../lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index 0c9675a14..a9e833855 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -125,9 +125,8 @@ 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("") { From 342f203c92a899f87c52b8b66de65da6ea7de41f Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Fri, 27 Jan 2023 12:43:47 +0100 Subject: [PATCH 7/7] code improvement, we only replace with zwsp when necessary --- .../Extensions/NSMutableAttributedString.swift | 14 ++++++++++---- .../Sources/HTMLParser/HTMLParser.swift | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSMutableAttributedString.swift index e09d61e9d..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 replaceDiscardableTextWithZwsp() { + /// 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.replaceCharacters(in: range, with: String.zwsp) + 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/HTMLParser.swift b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift index a9e833855..1e035dec9 100644 --- a/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift +++ b/platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParser.swift @@ -115,7 +115,7 @@ public final class HTMLParser { mutableAttributedString.applyBackgroundStyles(style: style) mutableAttributedString.applyInlineCodeBackgroundStyle(codeBackgroundColor: style.codeBackgroundColor) - mutableAttributedString.replaceDiscardableTextWithZwsp() + mutableAttributedString.replaceOrDeleteDiscardableText() mutableAttributedString.removeParagraphVerticalSpacing() removeTrailingNewlineIfNeeded(from: mutableAttributedString, given: html)