diff --git a/Sources/HTMLStreamer/AttributedStringConverter.swift b/Sources/HTMLStreamer/AttributedStringConverter.swift index a287c4d..9846804 100644 --- a/Sources/HTMLStreamer/AttributedStringConverter.swift +++ b/Sources/HTMLStreamer/AttributedStringConverter.swift @@ -27,6 +27,7 @@ public struct AttributedStringConverter { private var actionStack: [ElementAction] = [] private var styleStack: [Style] = [] private var previouslyFinishedBlockElement = false + private var currentElementIsEmpty = true // The current run of text w/o styles changing private var currentRun: String = "" @@ -49,13 +50,19 @@ public struct AttributedStringConverter { while let token = tokenizer.next() { switch token { case .character(let c): + currentElementIsEmpty = false + previouslyFinishedBlockElement = false currentRun.unicodeScalars.append(c) case .characterSequence(let s): + currentElementIsEmpty = false + previouslyFinishedBlockElement = false currentRun.append(s) case .comment: // ignored continue case .startTag(let name, let selfClosing, let attributes): + currentElementIsEmpty = true + previouslyFinishedBlockElement = false let action = Callbacks.elementAction(name: name, attributes: attributes) actionStack.append(action) handleStartTag(name, selfClosing: selfClosing, attributes: attributes) @@ -72,6 +79,9 @@ public struct AttributedStringConverter { } } + if previouslyFinishedBlockElement { + currentRun.removeLast(2) + } finishRun() return str @@ -111,21 +121,15 @@ public struct AttributedStringConverter { finishRun() styleStack.append(.monospace) case "pre": - startBlockElement() finishRun() styleStack.append(.monospace) case "blockquote": - startBlockElement() finishRun() styleStack.append(.blockquote) - case "p": - startBlockElement() case "ol": - startBlockElement() finishRun() styleStack.append(.orderedList(nextElementOrdinal: 1)) case "ul": - startBlockElement() finishRun() styleStack.append(.unorderedList) case "li": @@ -147,13 +151,6 @@ public struct AttributedStringConverter { } } - private mutating func startBlockElement() { - if str.length != 0 || !currentRun.isEmpty { - previouslyFinishedBlockElement = false - currentRun.append("\n\n") - } - } - private mutating func handleEndTag(_ name: String) { switch name { case "a": @@ -181,6 +178,8 @@ public struct AttributedStringConverter { finishRun() removeLastStyle(.blockquote) finishBlockElement() + case "p": + finishBlockElement() case "ol": finishRun() removeLastStyle(.orderedList) @@ -197,8 +196,9 @@ public struct AttributedStringConverter { } private mutating func finishBlockElement() { - if str.length != 0 { + if !currentElementIsEmpty { previouslyFinishedBlockElement = true + currentRun.append("\n\n") } } @@ -251,11 +251,6 @@ public struct AttributedStringConverter { currentRun = replacement } - if previouslyFinishedBlockElement { - previouslyFinishedBlockElement = false - currentRun.insert(contentsOf: "\n\n", at: currentRun.startIndex) - } - var attributes = [NSAttributedString.Key: Any]() var paragraphStyle = configuration.paragraphStyle var currentFontTraits: FontTrait = [] diff --git a/Sources/HTMLStreamer/TextConverter.swift b/Sources/HTMLStreamer/TextConverter.swift index 3f89151..1237625 100644 --- a/Sources/HTMLStreamer/TextConverter.swift +++ b/Sources/HTMLStreamer/TextConverter.swift @@ -16,6 +16,7 @@ public struct TextConverter { private var actionStack: [ElementAction] = [] private var previouslyFinishedBlockElement = false + private var currentElementIsEmpty = true private var currentRun = "" public init(configuration: TextConverterConfiguration = .init()) where Callbacks == DefaultCallbacks { @@ -33,10 +34,16 @@ public struct TextConverter { while let token = tokenizer.next() { switch token { case .character(let scalar): + currentElementIsEmpty = false + previouslyFinishedBlockElement = false currentRun.unicodeScalars.append(scalar) case .characterSequence(let string): + currentElementIsEmpty = false + previouslyFinishedBlockElement = false currentRun.append(string) case .startTag(let name, let selfClosing, let attributes): + currentElementIsEmpty = true + previouslyFinishedBlockElement = false let action = Callbacks.elementAction(name: name, attributes: attributes) actionStack.append(action) handleStartTag(name, selfClosing: selfClosing, attributes: attributes) @@ -51,6 +58,13 @@ public struct TextConverter { } } + if previouslyFinishedBlockElement { + if configuration.insertNewlines { + currentRun.removeLast(2) + } else { + currentRun.removeLast(1) + } + } finishRun() return str @@ -64,25 +78,11 @@ public struct TextConverter { } else { currentRun.append(" ") } - case "pre", "blockquote", "p", "ol", "ul": - startBlockElement() - finishRun() default: 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) { switch name { case "pre", "blockquote", "p", "ol", "ul": @@ -94,8 +94,13 @@ public struct TextConverter { } private mutating func finishBlockElement() { - if !str.isEmpty { + if !currentElementIsEmpty { previouslyFinishedBlockElement = true + if configuration.insertNewlines { + currentRun.append("\n\n") + } else { + currentRun.append(" ") + } } } @@ -111,15 +116,6 @@ public struct TextConverter { 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) currentRun = "" } diff --git a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift index 3cbc26b..041ae59 100644 --- a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift +++ b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift @@ -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("

inside
quote
after

"), result) + } + } diff --git a/Tests/HTMLStreamerTests/TextConverterTests.swift b/Tests/HTMLStreamerTests/TextConverterTests.swift index 485aa60..78faee4 100644 --- a/Tests/HTMLStreamerTests/TextConverterTests.swift +++ b/Tests/HTMLStreamerTests/TextConverterTests.swift @@ -63,4 +63,8 @@ final class TextConverterTests: XCTestCase { XCTAssertEqual(replaced, "…") } + func testEmptyBlockElements() { + XCTAssertEqual(convert("

inside
quote
after

"), "inside\nquote\n\nafter") + } + }