diff --git a/Sources/HTMLStreamer/AttributedStringConverter.swift b/Sources/HTMLStreamer/AttributedStringConverter.swift
index 5375086..9710dc8 100644
--- a/Sources/HTMLStreamer/AttributedStringConverter.swift
+++ b/Sources/HTMLStreamer/AttributedStringConverter.swift
@@ -17,7 +17,7 @@ private typealias PlatformFont = UIFont
private typealias PlatformFont = NSFont
#endif
-public struct AttributedStringConverter {
+public struct AttributedStringConverter: BlockRenderer {
private let configuration: AttributedStringConverterConfiguration
private var fontCache: [FontTrait: PlatformFont] = [:]
@@ -26,7 +26,7 @@ public struct AttributedStringConverter {
private var actionStack: [ElementAction] = []
private var styleStack: [Style] = []
- private var blockState = BlockState.unstarted
+ var blockState = BlockState.start
private var currentElementIsEmpty = true
private var previouslyFinishedListItem = false
// The current run of text w/o styles changing
@@ -46,7 +46,7 @@ public struct AttributedStringConverter {
actionStack = []
styleStack = []
- blockState = .unstarted
+ blockState = .start
currentElementIsEmpty = true
previouslyFinishedListItem = false
currentRun = ""
@@ -126,26 +126,28 @@ public struct AttributedStringConverter {
finishRun()
styleStack.append(.monospace)
case "pre":
- startBlockIfNecessary()
+ startOrFinishBlock()
finishRun()
styleStack.append(.monospace)
case "blockquote":
- startBlockIfNecessary()
+ startOrFinishBlock()
finishRun()
styleStack.append(.blockquote)
case "p":
- startBlockIfNecessary()
+ startOrFinishBlock()
case "ol":
- startBlockIfNecessary()
+ startOrFinishBlock()
finishRun()
styleStack.append(.orderedList(nextElementOrdinal: 1))
case "ul":
- startBlockIfNecessary()
+ startOrFinishBlock()
finishRun()
styleStack.append(.unorderedList)
case "li":
if previouslyFinishedListItem {
currentRun.append("\n")
+ } else {
+ continueBlock()
}
let marker: String
if case .orderedList(let nextElementOrdinal) = styleStack.last {
@@ -184,22 +186,22 @@ public struct AttributedStringConverter {
case "pre":
finishRun()
removeLastStyle(.monospace)
- finishBlockElement()
+ startOrFinishBlock()
case "blockquote":
finishRun()
removeLastStyle(.blockquote)
- finishBlockElement()
+ startOrFinishBlock()
case "p":
- finishBlockElement()
+ startOrFinishBlock()
case "ol":
finishRun()
removeLastStyle(.orderedList)
- finishBlockElement()
+ startOrFinishBlock()
previouslyFinishedListItem = false
case "ul":
finishRun()
removeLastStyle(.unorderedList)
- finishBlockElement()
+ startOrFinishBlock()
previouslyFinishedListItem = false
case "li":
finishRun()
@@ -209,42 +211,8 @@ public struct AttributedStringConverter {
}
}
- private mutating func startBlockIfNecessary() {
- switch blockState {
- case .unstarted:
- blockState = .started(false)
- case .started:
- break
- case .ongoing:
- currentRun.append("\n\n")
- blockState = .started(true)
- case .finished(let nonEmpty):
- if nonEmpty {
- currentRun.append("\n\n")
- }
- blockState = .started(nonEmpty)
- }
- }
-
- private mutating func continueBlock() {
- switch blockState {
- case .unstarted, .started(_):
- blockState = .ongoing
- case .ongoing:
- break
- case .finished(let nonEmpty):
- if nonEmpty {
- currentRun.append("\n\n")
- }
- blockState = .ongoing
- }
- }
-
- private mutating func finishBlockElement() {
- if blockState == .started(true) && currentElementIsEmpty {
- currentRun.removeLast(2)
- }
- blockState = .finished(blockState == .ongoing)
+ mutating func insertBlockBreak() {
+ currentRun.append("\n\n")
}
// Finds the last currently-open style of the given type.
@@ -452,13 +420,6 @@ private enum Style {
}
}
-enum BlockState: Equatable {
- case unstarted
- case started(Bool)
- case ongoing
- case finished(Bool)
-}
-
extension Collection where Element == Attribute {
public func attributeValue(for name: String) -> String? {
first(where: { $0.name == name })?.value
diff --git a/Sources/HTMLStreamer/BlockState.swift b/Sources/HTMLStreamer/BlockState.swift
new file mode 100644
index 0000000..51b4173
--- /dev/null
+++ b/Sources/HTMLStreamer/BlockState.swift
@@ -0,0 +1,45 @@
+//
+// BlockState.swift
+// HTMLStreamer
+//
+// Created by Shadowfacts on 2/14/24.
+//
+
+import Foundation
+
+protocol BlockRenderer {
+ var blockState: BlockState { get set }
+ mutating func insertBlockBreak()
+}
+
+extension BlockRenderer {
+ mutating func startOrFinishBlock() {
+ switch blockState {
+ case .start:
+ break
+ case .nonEmptyBlock:
+ blockState = .emptyBlock
+ case .emptyBlock:
+ break
+ }
+ }
+
+ mutating func continueBlock() {
+ switch blockState {
+ case .start:
+ blockState = .nonEmptyBlock
+ case .nonEmptyBlock:
+ break
+ case .emptyBlock:
+ insertBlockBreak()
+ blockState = .nonEmptyBlock
+ }
+ }
+
+}
+
+enum BlockState: Equatable {
+ case start
+ case nonEmptyBlock
+ case emptyBlock
+}
diff --git a/Sources/HTMLStreamer/TextConverter.swift b/Sources/HTMLStreamer/TextConverter.swift
index 8271aa6..57dac99 100644
--- a/Sources/HTMLStreamer/TextConverter.swift
+++ b/Sources/HTMLStreamer/TextConverter.swift
@@ -7,7 +7,7 @@
import Foundation
-public struct TextConverter {
+public struct TextConverter: BlockRenderer {
private let configuration: TextConverterConfiguration
@@ -15,7 +15,7 @@ public struct TextConverter {
private var str: String!
private var actionStack: [ElementAction] = []
- private var blockState = BlockState.unstarted
+ var blockState = BlockState.start
private var currentElementIsEmpty = true
private var currentRun = ""
@@ -31,7 +31,7 @@ public struct TextConverter {
tokenizer = Tokenizer(chars: html.unicodeScalars.makeIterator())
str = ""
- blockState = .unstarted
+ blockState = .start
currentElementIsEmpty = true
currentRun = ""
@@ -80,7 +80,7 @@ public struct TextConverter {
currentRun.append(" ")
}
case "pre", "blockquote", "p", "ol", "ul":
- startBlockIfNecessary()
+ startOrFinishBlock()
default:
break
}
@@ -89,66 +89,20 @@ public struct TextConverter {
private mutating func handleEndTag(_ name: String) {
switch name {
case "pre", "blockquote", "p", "ol", "ul":
- finishBlockElement()
+ startOrFinishBlock()
finishRun()
default:
break
}
}
- private mutating func startBlockIfNecessary() {
- switch blockState {
- case .unstarted:
- blockState = .started(false)
- case .started:
- break
- case .ongoing:
- if configuration.insertNewlines {
- currentRun.append("\n\n")
- } else {
- currentRun.append(" ")
- }
- blockState = .started(true)
- case .finished(let nonEmpty):
- if nonEmpty {
- if configuration.insertNewlines {
- currentRun.append("\n\n")
- } else {
- currentRun.append(" ")
- }
- }
- blockState = .started(nonEmpty)
+ mutating func insertBlockBreak() {
+ if configuration.insertNewlines {
+ currentRun.append("\n\n")
+ } else {
+ currentRun.append(" ")
}
}
-
- private mutating func continueBlock() {
- switch blockState {
- case .unstarted, .started(_):
- blockState = .ongoing
- case .ongoing:
- break
- case .finished(let nonEmpty):
- if nonEmpty {
- if configuration.insertNewlines {
- currentRun.append("\n\n")
- } else {
- currentRun.append(" ")
- }
- }
- blockState = .ongoing
- }
- }
-
- private mutating func finishBlockElement() {
- if blockState == .started(true) && currentElementIsEmpty {
- if configuration.insertNewlines {
- currentRun.removeLast(2)
- } else {
- currentRun.removeLast(1)
- }
- }
- blockState = .finished(blockState == .ongoing)
- }
private mutating func finishRun() {
if actionStack.contains(.skip) {
diff --git a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
index 6df0da9..2dacd30 100644
--- a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
+++ b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
@@ -206,20 +206,11 @@ final class AttributedStringConverterTests: XCTestCase {
}
func testMultipleBlockElements() {
- let result = NSMutableAttributedString()
- result.append(NSAttributedString(string: "a", attributes: [
+ let result = NSAttributedString(string: "a\n\nb", attributes: [
.font: italicFont,
.paragraphStyle: blockquoteParagraphStyle,
- ]))
- result.append(NSAttributedString(string: "\n\n", attributes: [
- .font: font,
- .paragraphStyle: NSParagraphStyle.default,
- ]))
- result.append(NSAttributedString(string: "b", attributes: [
- .font: italicFont,
- .paragraphStyle: blockquoteParagraphStyle,
- ]))
- result.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: result.length))
+ .foregroundColor: color,
+ ])
XCTAssertEqual(convert("a
b
"), result)
}
@@ -321,12 +312,12 @@ final class AttributedStringConverterTests: XCTestCase {
func testFollowedByList() {
let result = NSMutableAttributedString()
- result.append(NSAttributedString(string: "a\n\n", attributes: [
+ result.append(NSAttributedString(string: "a", attributes: [
.font: font,
.paragraphStyle: NSParagraphStyle.default,
.foregroundColor: color,
]))
- result.append(NSAttributedString(string: "\t1.\tb\n\t2.\tc", attributes: [
+ result.append(NSAttributedString(string: "\n\n\t1.\tb\n\t2.\tc", attributes: [
.font: font,
.paragraphStyle: listParagraphStyle,
.foregroundColor: color,
@@ -356,4 +347,19 @@ final class AttributedStringConverterTests: XCTestCase {
XCTAssertEqual(convert(""), .init())
}
+ func testWTF() {
+ let result = NSMutableAttributedString()
+ result.append(NSAttributedString(string: "a", attributes: [
+ .font: italicFont,
+ .paragraphStyle: blockquoteParagraphStyle,
+ .foregroundColor: color,
+ ]))
+ result.append(NSAttributedString(string: "\n\nb", attributes: [
+ .font: font,
+ .paragraphStyle: NSParagraphStyle.default,
+ .foregroundColor: color,
+ ]))
+ XCTAssertEqual(convert(#"a
b
"#), result)
+ }
+
}