From fa03efedbbfba123946548cdd08b211a2e220742 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 14 Feb 2024 21:07:19 -0500 Subject: [PATCH] Actually fix block element rendering --- .../AttributedStringConverter.swift | 73 +++++-------------- Sources/HTMLStreamer/BlockState.swift | 45 ++++++++++++ Sources/HTMLStreamer/TextConverter.swift | 66 +++-------------- .../AttributedStringConverterTests.swift | 34 +++++---- 4 files changed, 92 insertions(+), 126 deletions(-) create mode 100644 Sources/HTMLStreamer/BlockState.swift 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) + } + }