-
Notifications
You must be signed in to change notification settings - Fork 26
iOS: Replace NBSP with ZWSP to allow block rendering #520
Changes from all commits
67cf4e7
6399797
dcd024b
e2ad771
d587af1
66e029e
97feeeb
60102e6
ed3799d
342f203
c07e950
aee27c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
Comment on lines
+73
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you might be able to re-use the same text node after detaching it instead of creating a new one (no need to re-inherit/interpret attributes then) |
||
|
||
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
} | ||
Comment on lines
+115
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think this raises a Swiftlint warning, I'm guessing we can e.g. write something like: actualIndex -= discardableTextRanges
.filter { $0.upperBound <= attributedIndex }
.reduce(0) { $1 + $2.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 | ||
} | ||
} | ||
Comment on lines
+145
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here but more generally I'm sure we can globally improve the code here, and perhaps merge list prefixes and discardable text into the same Array so we don't have multiple arrays that achieve same kind of computation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is actually something missing in my mapping, because when I delete the last character in the last newline, the rendering is lost for that line, I think that I need just like for the list, create some kind of exception based on the cursor position, because deleting the last character probably is also deleting the ZWSP character and removing the rendering, would be nice to be able to keep it, and removing it alongside the newline There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah not sure what happens in that case, technically on backspace we do replace the HTML so we should be re-parsing and adding that ZWSP. |
||
|
||
let prefixes = listPrefixesRanges() | ||
var actualIndex: Int = htmlIndex | ||
|
||
for listPrefix in prefixes { | ||
if listPrefix.location < actualIndex { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can directly get the backgroundStyle alone here and avoid looking up the entire dictionary ourselves |
||
if attributes[.backgroundStyle] != nil { | ||
self.replaceCharacters(in: range, with: String.zwsp) | ||
} else { | ||
self.deleteCharacters(in: range) | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this could be replaced by