Actually fix block element rendering
This commit is contained in:
parent
1bfacb8fe9
commit
fa03efedbb
|
@ -17,7 +17,7 @@ private typealias PlatformFont = UIFont
|
||||||
private typealias PlatformFont = NSFont
|
private typealias PlatformFont = NSFont
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks>: BlockRenderer {
|
||||||
private let configuration: AttributedStringConverterConfiguration
|
private let configuration: AttributedStringConverterConfiguration
|
||||||
private var fontCache: [FontTrait: PlatformFont] = [:]
|
private var fontCache: [FontTrait: PlatformFont] = [:]
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
|
|
||||||
private var actionStack: [ElementAction] = []
|
private var actionStack: [ElementAction] = []
|
||||||
private var styleStack: [Style] = []
|
private var styleStack: [Style] = []
|
||||||
private var blockState = BlockState.unstarted
|
var blockState = BlockState.start
|
||||||
private var currentElementIsEmpty = true
|
private var currentElementIsEmpty = true
|
||||||
private var previouslyFinishedListItem = false
|
private var previouslyFinishedListItem = false
|
||||||
// The current run of text w/o styles changing
|
// The current run of text w/o styles changing
|
||||||
|
@ -46,7 +46,7 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
|
|
||||||
actionStack = []
|
actionStack = []
|
||||||
styleStack = []
|
styleStack = []
|
||||||
blockState = .unstarted
|
blockState = .start
|
||||||
currentElementIsEmpty = true
|
currentElementIsEmpty = true
|
||||||
previouslyFinishedListItem = false
|
previouslyFinishedListItem = false
|
||||||
currentRun = ""
|
currentRun = ""
|
||||||
|
@ -126,26 +126,28 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
finishRun()
|
finishRun()
|
||||||
styleStack.append(.monospace)
|
styleStack.append(.monospace)
|
||||||
case "pre":
|
case "pre":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
finishRun()
|
finishRun()
|
||||||
styleStack.append(.monospace)
|
styleStack.append(.monospace)
|
||||||
case "blockquote":
|
case "blockquote":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
finishRun()
|
finishRun()
|
||||||
styleStack.append(.blockquote)
|
styleStack.append(.blockquote)
|
||||||
case "p":
|
case "p":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
case "ol":
|
case "ol":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
finishRun()
|
finishRun()
|
||||||
styleStack.append(.orderedList(nextElementOrdinal: 1))
|
styleStack.append(.orderedList(nextElementOrdinal: 1))
|
||||||
case "ul":
|
case "ul":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
finishRun()
|
finishRun()
|
||||||
styleStack.append(.unorderedList)
|
styleStack.append(.unorderedList)
|
||||||
case "li":
|
case "li":
|
||||||
if previouslyFinishedListItem {
|
if previouslyFinishedListItem {
|
||||||
currentRun.append("\n")
|
currentRun.append("\n")
|
||||||
|
} else {
|
||||||
|
continueBlock()
|
||||||
}
|
}
|
||||||
let marker: String
|
let marker: String
|
||||||
if case .orderedList(let nextElementOrdinal) = styleStack.last {
|
if case .orderedList(let nextElementOrdinal) = styleStack.last {
|
||||||
|
@ -184,22 +186,22 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
case "pre":
|
case "pre":
|
||||||
finishRun()
|
finishRun()
|
||||||
removeLastStyle(.monospace)
|
removeLastStyle(.monospace)
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
case "blockquote":
|
case "blockquote":
|
||||||
finishRun()
|
finishRun()
|
||||||
removeLastStyle(.blockquote)
|
removeLastStyle(.blockquote)
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
case "p":
|
case "p":
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
case "ol":
|
case "ol":
|
||||||
finishRun()
|
finishRun()
|
||||||
removeLastStyle(.orderedList)
|
removeLastStyle(.orderedList)
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
previouslyFinishedListItem = false
|
previouslyFinishedListItem = false
|
||||||
case "ul":
|
case "ul":
|
||||||
finishRun()
|
finishRun()
|
||||||
removeLastStyle(.unorderedList)
|
removeLastStyle(.unorderedList)
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
previouslyFinishedListItem = false
|
previouslyFinishedListItem = false
|
||||||
case "li":
|
case "li":
|
||||||
finishRun()
|
finishRun()
|
||||||
|
@ -209,42 +211,8 @@ public struct AttributedStringConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func startBlockIfNecessary() {
|
mutating func insertBlockBreak() {
|
||||||
switch blockState {
|
currentRun.append("\n\n")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finds the last currently-open style of the given type.
|
// 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 {
|
extension Collection where Element == Attribute {
|
||||||
public func attributeValue(for name: String) -> String? {
|
public func attributeValue(for name: String) -> String? {
|
||||||
first(where: { $0.name == name })?.value
|
first(where: { $0.name == name })?.value
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
|
public struct TextConverter<Callbacks: HTMLConversionCallbacks>: BlockRenderer {
|
||||||
|
|
||||||
private let configuration: TextConverterConfiguration
|
private let configuration: TextConverterConfiguration
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
private var str: String!
|
private var str: String!
|
||||||
|
|
||||||
private var actionStack: [ElementAction] = []
|
private var actionStack: [ElementAction] = []
|
||||||
private var blockState = BlockState.unstarted
|
var blockState = BlockState.start
|
||||||
private var currentElementIsEmpty = true
|
private var currentElementIsEmpty = true
|
||||||
private var currentRun = ""
|
private var currentRun = ""
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
tokenizer = Tokenizer(chars: html.unicodeScalars.makeIterator())
|
tokenizer = Tokenizer(chars: html.unicodeScalars.makeIterator())
|
||||||
str = ""
|
str = ""
|
||||||
|
|
||||||
blockState = .unstarted
|
blockState = .start
|
||||||
currentElementIsEmpty = true
|
currentElementIsEmpty = true
|
||||||
currentRun = ""
|
currentRun = ""
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
currentRun.append(" ")
|
currentRun.append(" ")
|
||||||
}
|
}
|
||||||
case "pre", "blockquote", "p", "ol", "ul":
|
case "pre", "blockquote", "p", "ol", "ul":
|
||||||
startBlockIfNecessary()
|
startOrFinishBlock()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -89,66 +89,20 @@ public struct TextConverter<Callbacks: HTMLConversionCallbacks> {
|
||||||
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":
|
||||||
finishBlockElement()
|
startOrFinishBlock()
|
||||||
finishRun()
|
finishRun()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func startBlockIfNecessary() {
|
mutating func insertBlockBreak() {
|
||||||
switch blockState {
|
if configuration.insertNewlines {
|
||||||
case .unstarted:
|
currentRun.append("\n\n")
|
||||||
blockState = .started(false)
|
} else {
|
||||||
case .started:
|
currentRun.append(" ")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private mutating func finishRun() {
|
||||||
if actionStack.contains(.skip) {
|
if actionStack.contains(.skip) {
|
||||||
|
|
|
@ -206,20 +206,11 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleBlockElements() {
|
func testMultipleBlockElements() {
|
||||||
let result = NSMutableAttributedString()
|
let result = NSAttributedString(string: "a\n\nb", attributes: [
|
||||||
result.append(NSAttributedString(string: "a", attributes: [
|
|
||||||
.font: italicFont,
|
.font: italicFont,
|
||||||
.paragraphStyle: blockquoteParagraphStyle,
|
.paragraphStyle: blockquoteParagraphStyle,
|
||||||
]))
|
.foregroundColor: color,
|
||||||
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))
|
|
||||||
XCTAssertEqual(convert("<blockquote>a</blockquote><blockquote>b</blockquote>"), result)
|
XCTAssertEqual(convert("<blockquote>a</blockquote><blockquote>b</blockquote>"), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,12 +312,12 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
|
|
||||||
func testFollowedByList() {
|
func testFollowedByList() {
|
||||||
let result = NSMutableAttributedString()
|
let result = NSMutableAttributedString()
|
||||||
result.append(NSAttributedString(string: "a\n\n", attributes: [
|
result.append(NSAttributedString(string: "a", attributes: [
|
||||||
.font: font,
|
.font: font,
|
||||||
.paragraphStyle: NSParagraphStyle.default,
|
.paragraphStyle: NSParagraphStyle.default,
|
||||||
.foregroundColor: color,
|
.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,
|
.font: font,
|
||||||
.paragraphStyle: listParagraphStyle,
|
.paragraphStyle: listParagraphStyle,
|
||||||
.foregroundColor: color,
|
.foregroundColor: color,
|
||||||
|
@ -356,4 +347,19 @@ final class AttributedStringConverterTests: XCTestCase {
|
||||||
XCTAssertEqual(convert("</span>"), .init())
|
XCTAssertEqual(convert("</span>"), .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(#"<blockquote><p>a</p></blockquote><p>b</p>"#), result)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue