From 95edb408fad0f48f3ac19e202429261c16f15372 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 17 Mar 2024 10:56:49 -0400 Subject: [PATCH] Fix replace callback with nested element, fix skip/replace at beginning of block skipping block break Fixes shadowfacts/Tusker#461 --- .../AttributedStringConverter.swift | 29 +++++++---- .../HTMLConversionCallbacks.swift | 8 --- Sources/HTMLStreamer/TextConverter.swift | 29 +++++++---- .../AttributedStringConverterTests.swift | 50 +++++++++++++++++++ 4 files changed, 90 insertions(+), 26 deletions(-) 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("
  1. 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

c

+""" + 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) + } + }