Actually fix how whitespace is inserted between block elements

This commit is contained in:
Shadowfacts 2023-12-26 14:29:40 -05:00
parent 1ee7ab9405
commit b33be0f178
4 changed files with 53 additions and 43 deletions

View File

@ -27,6 +27,7 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
private var actionStack: [ElementAction] = [] private var actionStack: [ElementAction] = []
private var styleStack: [Style] = [] private var styleStack: [Style] = []
private var previouslyFinishedBlockElement = false private var previouslyFinishedBlockElement = false
private var currentElementIsEmpty = true
// The current run of text w/o styles changing // The current run of text w/o styles changing
private var currentRun: String = "" private var currentRun: String = ""
@ -49,13 +50,19 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
while let token = tokenizer.next() { while let token = tokenizer.next() {
switch token { switch token {
case .character(let c): case .character(let c):
currentElementIsEmpty = false
previouslyFinishedBlockElement = false
currentRun.unicodeScalars.append(c) currentRun.unicodeScalars.append(c)
case .characterSequence(let s): case .characterSequence(let s):
currentElementIsEmpty = false
previouslyFinishedBlockElement = false
currentRun.append(s) currentRun.append(s)
case .comment: case .comment:
// ignored // ignored
continue continue
case .startTag(let name, let selfClosing, let attributes): case .startTag(let name, let selfClosing, let attributes):
currentElementIsEmpty = true
previouslyFinishedBlockElement = false
let action = Callbacks.elementAction(name: name, attributes: attributes) let action = Callbacks.elementAction(name: name, attributes: attributes)
actionStack.append(action) actionStack.append(action)
handleStartTag(name, selfClosing: selfClosing, attributes: attributes) handleStartTag(name, selfClosing: selfClosing, attributes: attributes)
@ -72,6 +79,9 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
} }
} }
if previouslyFinishedBlockElement {
currentRun.removeLast(2)
}
finishRun() finishRun()
return str return str
@ -111,21 +121,15 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
finishRun() finishRun()
styleStack.append(.monospace) styleStack.append(.monospace)
case "pre": case "pre":
startBlockElement()
finishRun() finishRun()
styleStack.append(.monospace) styleStack.append(.monospace)
case "blockquote": case "blockquote":
startBlockElement()
finishRun() finishRun()
styleStack.append(.blockquote) styleStack.append(.blockquote)
case "p":
startBlockElement()
case "ol": case "ol":
startBlockElement()
finishRun() finishRun()
styleStack.append(.orderedList(nextElementOrdinal: 1)) styleStack.append(.orderedList(nextElementOrdinal: 1))
case "ul": case "ul":
startBlockElement()
finishRun() finishRun()
styleStack.append(.unorderedList) styleStack.append(.unorderedList)
case "li": case "li":
@ -147,13 +151,6 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
} }
} }
private mutating func startBlockElement() {
if str.length != 0 || !currentRun.isEmpty {
previouslyFinishedBlockElement = false
currentRun.append("\n\n")
}
}
private mutating func handleEndTag(_ name: String) { private mutating func handleEndTag(_ name: String) {
switch name { switch name {
case "a": case "a":
@ -181,6 +178,8 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
finishRun() finishRun()
removeLastStyle(.blockquote) removeLastStyle(.blockquote)
finishBlockElement() finishBlockElement()
case "p":
finishBlockElement()
case "ol": case "ol":
finishRun() finishRun()
removeLastStyle(.orderedList) removeLastStyle(.orderedList)
@ -197,8 +196,9 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
} }
private mutating func finishBlockElement() { private mutating func finishBlockElement() {
if str.length != 0 { if !currentElementIsEmpty {
previouslyFinishedBlockElement = true previouslyFinishedBlockElement = true
currentRun.append("\n\n")
} }
} }
@ -251,11 +251,6 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
currentRun = replacement currentRun = replacement
} }
if previouslyFinishedBlockElement {
previouslyFinishedBlockElement = false
currentRun.insert(contentsOf: "\n\n", at: currentRun.startIndex)
}
var attributes = [NSAttributedString.Key: Any]() var attributes = [NSAttributedString.Key: Any]()
var paragraphStyle = configuration.paragraphStyle var paragraphStyle = configuration.paragraphStyle
var currentFontTraits: FontTrait = [] var currentFontTraits: FontTrait = []

View File

@ -16,6 +16,7 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
private var actionStack: [ElementAction] = [] private var actionStack: [ElementAction] = []
private var previouslyFinishedBlockElement = false private var previouslyFinishedBlockElement = false
private var currentElementIsEmpty = true
private var currentRun = "" private var currentRun = ""
public init(configuration: TextConverterConfiguration = .init()) where Callbacks == DefaultCallbacks { public init(configuration: TextConverterConfiguration = .init()) where Callbacks == DefaultCallbacks {
@ -33,10 +34,16 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
while let token = tokenizer.next() { while let token = tokenizer.next() {
switch token { switch token {
case .character(let scalar): case .character(let scalar):
currentElementIsEmpty = false
previouslyFinishedBlockElement = false
currentRun.unicodeScalars.append(scalar) currentRun.unicodeScalars.append(scalar)
case .characterSequence(let string): case .characterSequence(let string):
currentElementIsEmpty = false
previouslyFinishedBlockElement = false
currentRun.append(string) currentRun.append(string)
case .startTag(let name, let selfClosing, let attributes): case .startTag(let name, let selfClosing, let attributes):
currentElementIsEmpty = true
previouslyFinishedBlockElement = false
let action = Callbacks.elementAction(name: name, attributes: attributes) let action = Callbacks.elementAction(name: name, attributes: attributes)
actionStack.append(action) actionStack.append(action)
handleStartTag(name, selfClosing: selfClosing, attributes: attributes) handleStartTag(name, selfClosing: selfClosing, attributes: attributes)
@ -51,6 +58,13 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
} }
} }
if previouslyFinishedBlockElement {
if configuration.insertNewlines {
currentRun.removeLast(2)
} else {
currentRun.removeLast(1)
}
}
finishRun() finishRun()
return str return str
@ -64,25 +78,11 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
} else { } else {
currentRun.append(" ") currentRun.append(" ")
} }
case "pre", "blockquote", "p", "ol", "ul":
startBlockElement()
finishRun()
default: default:
break break
} }
} }
private mutating func startBlockElement() {
if !str.isEmpty {
previouslyFinishedBlockElement = false
if configuration.insertNewlines {
currentRun.append("\n\n")
} else {
currentRun.append(" ")
}
}
}
private mutating func handleEndTag(_ name: String) { private mutating func handleEndTag(_ name: String) {
switch name { switch name {
case "pre", "blockquote", "p", "ol", "ul": case "pre", "blockquote", "p", "ol", "ul":
@ -94,8 +94,13 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
} }
private mutating func finishBlockElement() { private mutating func finishBlockElement() {
if !str.isEmpty { if !currentElementIsEmpty {
previouslyFinishedBlockElement = true previouslyFinishedBlockElement = true
if configuration.insertNewlines {
currentRun.append("\n\n")
} else {
currentRun.append(" ")
}
} }
} }
@ -111,15 +116,6 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
currentRun = replacement currentRun = replacement
} }
if previouslyFinishedBlockElement {
previouslyFinishedBlockElement = false
if configuration.insertNewlines {
currentRun.insert(contentsOf: "\n\n", at: currentRun.startIndex)
} else {
currentRun.insert(" ", at: currentRun.startIndex)
}
}
str.append(currentRun) str.append(currentRun)
currentRun = "" currentRun = ""
} }

View File

@ -290,4 +290,19 @@ final class AttributedStringConverterTests: XCTestCase {
])) ]))
} }
func testEmptyBlockElements() {
let result = NSMutableAttributedString()
result.append(NSAttributedString(string: "inside\nquote", attributes: [
.font: italicFont,
.paragraphStyle: blockquoteParagraphStyle,
.foregroundColor: color,
]))
result.append(NSAttributedString(string: "\n\nafter", attributes: [
.font: font,
.paragraphStyle: NSParagraphStyle.default,
.foregroundColor: color,
]))
XCTAssertEqual(convert("<p></p><blockquote><span>inside<br>quote</span></blockquote><span>after</span><p></p>"), result)
}
} }

View File

@ -63,4 +63,8 @@ final class TextConverterTests: XCTestCase {
XCTAssertEqual(replaced, "") XCTAssertEqual(replaced, "")
} }
func testEmptyBlockElements() {
XCTAssertEqual(convert("<p></p><blockquote><span>inside<br>quote</span></blockquote><span>after</span><p></p>"), "inside\nquote\n\nafter")
}
} }