diff --git a/Sources/HTMLStreamer/AttributedStringConverter.swift b/Sources/HTMLStreamer/AttributedStringConverter.swift
index f9dd781..393e6d0 100644
--- a/Sources/HTMLStreamer/AttributedStringConverter.swift
+++ b/Sources/HTMLStreamer/AttributedStringConverter.swift
@@ -24,7 +24,19 @@ public class AttributedStringConverter {
private var tokenizer: Tokenizer!
private var str: NSMutableAttributedString!
- private var actionStack: [ElementAction] = []
+ private var actionStack: [ElementAction] = [] {
+ didSet {
+ hasSkipOrReplaceElementAction = actionStack.contains(where: {
+ switch $0 {
+ case .skip, .replace(_):
+ true
+ default:
+ false
+ }
+ })
+ }
+ }
+ private var hasSkipOrReplaceElementAction = false
private var styleStack: [Style] = []
private var blockStateMachine = BlockStateMachine(blockBreak: "", lineBreak: "", listIndentForContentOutsideItem: "", append: { _ in }, removeChar: {})
private var currentElementIsEmpty = true
@@ -59,13 +71,15 @@ public class AttributedStringConverter {
switch token {
case .character(let c):
currentElementIsEmpty = false
- if blockStateMachine.continueBlock(char: c) {
+ if blockStateMachine.continueBlock(char: c),
+ !hasSkipOrReplaceElementAction {
currentRun.unicodeScalars.append(c)
}
case .characterSequence(let s):
currentElementIsEmpty = false
for c in s.unicodeScalars {
- if blockStateMachine.continueBlock(char: c) {
+ if blockStateMachine.continueBlock(char: c),
+ !hasSkipOrReplaceElementAction {
currentRun.unicodeScalars.append(c)
}
}
@@ -281,13 +295,10 @@ public class AttributedStringConverter {
}()
private func finishRun() {
- if actionStack.contains(.skip) {
- currentRun = ""
- return
- } else if case .append(let s) = actionStack.last {
+ if case .append(let s) = actionStack.last {
currentRun.append(s)
- } else if case .replace(let replacement) = actionStack.first(where: \.isReplace) {
- currentRun = replacement
+ } else if case .replace(let replacement) = actionStack.last {
+ currentRun.append(replacement)
}
guard !currentRun.isEmpty else {
diff --git a/Sources/HTMLStreamer/HTMLConversionCallbacks.swift b/Sources/HTMLStreamer/HTMLConversionCallbacks.swift
index a2db8ab..6e55898 100644
--- a/Sources/HTMLStreamer/HTMLConversionCallbacks.swift
+++ b/Sources/HTMLStreamer/HTMLConversionCallbacks.swift
@@ -17,14 +17,6 @@ public enum ElementAction: Equatable {
case skip
case replace(String)
case append(String)
-
- var isReplace: Bool {
- if case .replace(_) = self {
- true
- } else {
- false
- }
- }
}
public extension HTMLConversionCallbacks {
diff --git a/Sources/HTMLStreamer/TextConverter.swift b/Sources/HTMLStreamer/TextConverter.swift
index 267bb1b..f446c62 100644
--- a/Sources/HTMLStreamer/TextConverter.swift
+++ b/Sources/HTMLStreamer/TextConverter.swift
@@ -13,7 +13,19 @@ public class TextConverter {
private var tokenizer: Tokenizer!
private var str: String!
- private var actionStack: [ElementAction] = []
+ private var actionStack: [ElementAction] = [] {
+ didSet {
+ hasSkipOrReplaceElementAction = actionStack.contains(where: {
+ switch $0 {
+ case .skip, .replace(_):
+ true
+ default:
+ false
+ }
+ })
+ }
+ }
+ private var hasSkipOrReplaceElementAction = false
var blockStateMachine = BlockStateMachine(blockBreak: "", lineBreak: "", listIndentForContentOutsideItem: "", append: { _ in }, removeChar: {})
private var currentElementIsEmpty = true
private var currentRun = ""
@@ -46,13 +58,15 @@ public class TextConverter {
switch token {
case .character(let scalar):
currentElementIsEmpty = false
- if blockStateMachine.continueBlock(char: scalar) {
+ if blockStateMachine.continueBlock(char: scalar),
+ !hasSkipOrReplaceElementAction {
currentRun.unicodeScalars.append(scalar)
}
case .characterSequence(let string):
currentElementIsEmpty = false
for c in string.unicodeScalars {
- if blockStateMachine.continueBlock(char: c) {
+ if blockStateMachine.continueBlock(char: c),
+ !hasSkipOrReplaceElementAction {
currentRun.unicodeScalars.append(c)
}
}
@@ -137,13 +151,10 @@ public class TextConverter {
}
private func finishRun() {
- if actionStack.contains(.skip) {
- currentRun = ""
- return
- } else if case .append(let s) = actionStack.last {
+ if case .append(let s) = actionStack.last {
currentRun.append(s)
- } else if case .replace(let replacement) = actionStack.first(where: \.isReplace) {
- currentRun = replacement
+ } else if case .replace(let replacement) = actionStack.last {
+ currentRun.append(replacement)
}
guard !currentRun.isEmpty else {
diff --git a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
index 9a5eb3d..f1e2829 100644
--- a/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
+++ b/Tests/HTMLStreamerTests/AttributedStringConverterTests.swift
@@ -264,6 +264,12 @@ final class AttributedStringConverterTests: XCTestCase {
.paragraphStyle: NSParagraphStyle.default,
.foregroundColor: color,
]))
+ let replaceNested = convert("a", callbacks: Callbacks.self)
+ XCTAssertEqual(replaceNested, NSAttributedString(string: "…", attributes: [
+ .font: font,
+ .paragraphStyle: NSParagraphStyle.default,
+ .foregroundColor: color,
+ ]))
let appended = convert("test", callbacks: Callbacks.self)
XCTAssertEqual(appended, NSAttributedString(string: "test…", attributes: [
.font: font,
@@ -612,4 +618,48 @@ final class AttributedStringConverterTests: XCTestCase {
XCTAssertEqual(convert("a
"), result)
}
+ func testInvisibleAtBeginningOfParagraphDoesNotPreventParagraphBreak() {
+ struct Invisible: HTMLConversionCallbacks {
+ static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
+ if attributes.attributeValue(for: "class") == "invisible" {
+ .skip
+ } else {
+ .default
+ }
+ }
+ }
+
+ let result = NSAttributedString(string: "a\n\nc", attributes: [
+ .font: font,
+ .paragraphStyle: NSParagraphStyle.default,
+ .foregroundColor: color,
+ ])
+ let html = """
+a
bc
+"""
+ XCTAssertEqual(convert(html, callbacks: Invisible.self), result)
+ }
+
+ func testReplaceAtBeginningOfParagraphDoesNotPreventParagraphBreak() {
+ struct Replace: HTMLConversionCallbacks {
+ static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
+ if attributes.attributeValue(for: "class") == "replace" {
+ .replace("c")
+ } else {
+ .default
+ }
+ }
+ }
+
+ let result = NSAttributedString(string: "a\n\nc", attributes: [
+ .font: font,
+ .paragraphStyle: NSParagraphStyle.default,
+ .foregroundColor: color,
+ ])
+ let html = """
+a
b
+"""
+ XCTAssertEqual(convert(html, callbacks: Replace.self), result)
+ }
+
}