Actually fix how whitespace is inserted between block elements
This commit is contained in:
parent
1ee7ab9405
commit
b33be0f178
|
@ -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 = []
|
||||||
|
|
|
@ -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 = ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue