diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 1d28546591b6..47ebf3c10ed2 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5a94a8cef753a6b67c6281c7661bb654a9e97e34a4cee0ea42071d0e1a72f474", + "originHash" : "dda5c0004d2c337ca5d411fa9c103eb10933935ce58df86451e2786492abcd51", "pins" : [ { "identity" : "alamofire", @@ -309,8 +309,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "3c2c7e1e72b8abd96eafbae80323c5c1e5317437", - "version" : "2.7.5" + "revision" : "ead56133a693d0184d8c2db1a6d6394410cacfd6", + "version" : "2.13.6" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 7c5caa5d05fe..9d07d6b80bb7 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -12,6 +12,7 @@ let package = Package( .library(name: "AsyncImageKit", targets: ["AsyncImageKit"]), .library(name: "DesignSystem", targets: ["DesignSystem"]), .library(name: "FormattableContentKit", targets: ["FormattableContentKit"]), + .library(name: "GutenbergProcessors", targets: ["GutenbergProcessors"]), .library(name: "JetpackStats", targets: ["JetpackStats"]), .library(name: "JetpackSocial", targets: ["JetpackSocial"]), .library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]), @@ -46,7 +47,7 @@ let package = Package( .package(url: "https://github.com/erikdoe/ocmock", revision: "2c0bfd373289f4a7716db5d6db471640f91a6507"), .package(url: "https://github.com/johnxnguyen/Down", branch: "master"), .package(url: "https://github.com/kaishin/Gifu", from: "3.4.1"), - .package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.7.5"), + .package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.13.6"), .package(url: "https://github.com/squarefrog/UIDeviceIdentifier", from: "2.3.0"), // We can remove the SVProgressHUD fork once this PR is merged: https://github.com/SVProgressHUD/SVProgressHUD/pull/1131 .package(url: "https://github.com/automattic/SVProgressHUD", branch: "master"), @@ -334,6 +335,11 @@ let package = Package( .enableUpcomingFeature("BareSlashRegexLiterals") ] ), + .target( + name: "GutenbergProcessors", + dependencies: [.product(name: "SwiftSoup", package: "SwiftSoup")], + swiftSettings: [.swiftLanguageMode(.v5)] + ), .target( name: "WordPressReader", dependencies: [ @@ -400,6 +406,14 @@ let package = Package( ), .testTarget(name: "WordPressCoreTests", dependencies: [.target(name: "WordPressCore")]), .testTarget(name: "WordPressIntelligenceTests", dependencies: [.target(name: "WordPressIntelligence")]), + .testTarget( + name: "GutenbergProcessorsTests", + dependencies: [ + .target(name: "GutenbergProcessors"), + .product(name: "SwiftSoup", package: "SwiftSoup") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), .testTarget(name: "WordPressReaderTests", dependencies: [.target(name: "WordPressReader")]), .testTarget( name: "JetpackSocialTests", @@ -484,6 +498,7 @@ enum XcodeSupport { "DesignSystem", "BuildSettingsKit", "FormattableContentKit", + "GutenbergProcessors", "JetpackSocial", "JetpackStats", "JetpackStatsWidgetsCore", diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergContentParser.swift b/Modules/Sources/GutenbergProcessors/GutenbergContentParser.swift similarity index 61% rename from WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergContentParser.swift rename to Modules/Sources/GutenbergProcessors/GutenbergContentParser.swift index ac4b85c44b87..55e7ec9cd7d2 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergContentParser.swift +++ b/Modules/Sources/GutenbergProcessors/GutenbergContentParser.swift @@ -10,9 +10,9 @@ public class GutenbergParsedBlock { public var attributes: [String: Any] { get { - guard let data = self.attributesData.data(using: .utf8 ), - let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), - let attributes = jsonObject as? [String: Any] + guard let data = self.attributesData.data(using: .utf8), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), + let attributes = jsonObject as? [String: Any] else { return [:] } @@ -21,7 +21,8 @@ public class GutenbergParsedBlock { set(newValue) { guard let data = try? JSONSerialization.data(withJSONObject: newValue, options: .sortedKeys), - let attributes = String(data: data, encoding: .utf8) else { + let attributes = String(data: data, encoding: .utf8) + else { return } self.attributesData = attributes @@ -44,8 +45,7 @@ public class GutenbergParsedBlock { if let separatorRange = data.range(of: " ") { self.name = String(data[data.startIndex.. String { - return (try? self.htmlDocument?.body()?.html()) ?? "" + guard let body = self.htmlDocument?.body() else { + return "" + } + // SwiftSoup 2.12+ serializes unchanged nodes from a cached copy of the + // original source, so the attribute mutations the processors make on + // nested elements aren't reflected in the output. Replacing each + // top-level element with a copy marks its subtree dirty and forces a + // re-render, while the surrounding comment and text nodes (the Gutenberg + // block delimiters) are emitted from their original bytes. + for element in body.children().array() { + guard let clone = try? element.copy() as? Element else { + continue + } + try? element.replaceWith(clone) + } + return (try? body.html()) ?? "" } private func traverseChildNodes(element: Element, parentBlock: GutenbergParsedBlock? = nil) { var currentBlock: GutenbergParsedBlock? - element.getChildNodes().forEach { node in - switch node { - // Convert comment tag into block - case let comment as SwiftSoup.Comment: - guard let block = GutenbergParsedBlock(comment: comment, parentBlock: parentBlock) else { - return - } + element.getChildNodes() + .forEach { node in + switch node { + // Convert comment tag into block + case let comment as SwiftSoup.Comment: + guard let block = GutenbergParsedBlock(comment: comment, parentBlock: parentBlock) else { + return + } - // Identify close tag - if let currrentBlock = currentBlock, block.name == "/\(currrentBlock.name)" { - currentBlock = nil - return - } + // Identify close tag + if let currrentBlock = currentBlock, block.name == "/\(currrentBlock.name)" { + currentBlock = nil + return + } - self.blocks.append(block) - currentBlock = block - // Insert HTML elements into block being processed - case let element as SwiftSoup.Element: - if let currentBlock { - currentBlock.elements.add(element) + self.blocks.append(block) + currentBlock = block + // Insert HTML elements into block being processed + case let element as SwiftSoup.Element: + if let currentBlock { + currentBlock.elements.add(element) + } + if element.childNodeSize() > 0 { + traverseChildNodes(element: element, parentBlock: currentBlock ?? parentBlock) + } + default: break } - if element.childNodeSize() > 0 { - traverseChildNodes(element: element, parentBlock: currentBlock ?? parentBlock) - } - default: break } - } } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift b/Modules/Sources/GutenbergProcessors/GutenbergFileUploadProcessor.swift similarity index 84% rename from WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift rename to Modules/Sources/GutenbergProcessors/GutenbergFileUploadProcessor.swift index 7a16f8ac56aa..4417ec007908 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift +++ b/Modules/Sources/GutenbergProcessors/GutenbergFileUploadProcessor.swift @@ -1,6 +1,6 @@ import Foundation -class GutenbergFileUploadProcessor: GutenbergProcessor { +public class GutenbergFileUploadProcessor: GutenbergProcessor { private struct FileBlockKeys { static var name = "wp:file" static var id = "id" @@ -11,7 +11,7 @@ class GutenbergFileUploadProcessor: GutenbergProcessor { let remoteURLString: String let serverMediaID: Int - init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) { + public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) { self.mediaUploadID = mediaUploadID self.serverMediaID = serverMediaID self.remoteURLString = remoteURLString @@ -34,7 +34,7 @@ class GutenbergFileUploadProcessor: GutenbergProcessor { } } - func process(_ blocks: [GutenbergParsedBlock]) { + public func process(_ blocks: [GutenbergParsedBlock]) { processFileBlocks(blocks) } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift b/Modules/Sources/GutenbergProcessors/GutenbergGalleryUploadProcessor.swift similarity index 95% rename from WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift rename to Modules/Sources/GutenbergProcessors/GutenbergGalleryUploadProcessor.swift index 3661750eecb9..2847ad9687bc 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift +++ b/Modules/Sources/GutenbergProcessors/GutenbergGalleryUploadProcessor.swift @@ -1,7 +1,7 @@ import Foundation import SwiftSoup -class GutenbergGalleryUploadProcessor: GutenbergProcessor { +public class GutenbergGalleryUploadProcessor: GutenbergProcessor { let mediaUploadID: Int32 let remoteURLString: String @@ -12,7 +12,7 @@ class GutenbergGalleryUploadProcessor: GutenbergProcessor { static let imgClassIDPrefixAttribute = "wp-image-" - init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String, mediaLink: String) { + public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String, mediaLink: String) { self.mediaUploadID = mediaUploadID self.serverMediaID = serverMediaID self.remoteURLString = remoteURLString @@ -139,7 +139,7 @@ class GutenbergGalleryUploadProcessor: GutenbergProcessor { } } - func process(_ blocks: [GutenbergParsedBlock]) { + public func process(_ blocks: [GutenbergParsedBlock]) { processGalleryBlocks(blocks) } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift b/Modules/Sources/GutenbergProcessors/GutenbergImgUploadProcessor.swift similarity index 91% rename from WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift rename to Modules/Sources/GutenbergProcessors/GutenbergImgUploadProcessor.swift index 88b5e84e3054..593c9a8b5392 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift +++ b/Modules/Sources/GutenbergProcessors/GutenbergImgUploadProcessor.swift @@ -1,13 +1,13 @@ import Foundation -class GutenbergImgUploadProcessor: GutenbergProcessor { +public class GutenbergImgUploadProcessor: GutenbergProcessor { let mediaUploadID: Int32 let remoteURLString: String let serverMediaID: Int static let imgClassIDPrefixAttribute = "wp-image-" - init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) { + public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) { self.mediaUploadID = mediaUploadID self.serverMediaID = serverMediaID self.remoteURLString = remoteURLString @@ -63,7 +63,7 @@ class GutenbergImgUploadProcessor: GutenbergProcessor { } } - func process(_ blocks: [GutenbergParsedBlock]) { + public func process(_ blocks: [GutenbergParsedBlock]) { processImageBlocks(blocks) processMediaTextBlocks(blocks) } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergProcessor.swift b/Modules/Sources/GutenbergProcessors/GutenbergProcessor.swift similarity index 100% rename from WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergProcessor.swift rename to Modules/Sources/GutenbergProcessors/GutenbergProcessor.swift diff --git a/Modules/Tests/GutenbergProcessorsTests/GutenbergContentParserTests.swift b/Modules/Tests/GutenbergProcessorsTests/GutenbergContentParserTests.swift new file mode 100644 index 000000000000..53c8390387d4 --- /dev/null +++ b/Modules/Tests/GutenbergProcessorsTests/GutenbergContentParserTests.swift @@ -0,0 +1,351 @@ +import XCTest +@testable import GutenbergProcessors +import SwiftSoup + +class GutenbergContentParserTests: XCTestCase { + let singleBlock = """ + +

Hello world!

+ + """ + + let nestedBlock = """ + +
+
+

Title

+ + +

This is a nested block.

+ + +
+ +

Subtitle

+ + +

This is another nested block.

+ + +

Footer

+
+
+ + """ + + func testParserSingleBlock() { + let parser = GutenbergContentParser(for: singleBlock) + let blocks = parser.blocks + + let expectedBlockContent = """ +

Hello world!

+ """ + + XCTAssertEqual(blocks.count, 1, "Should return one block") + + XCTAssertEqual(blocks[0].name, "wp:block", "Name should match block's name") + XCTAssertEqual(blocks[0].content, expectedBlockContent, "Content should match block's content") + XCTAssertEqual(blocks[0].attributes.count, 1, "Attributes should contain one item") + XCTAssertEqual(blocks[0].attributes["id"] as? Int, 1, "Id attribute matches block's attribute") + XCTAssertEqual(blocks[0].blocks.count, 0, "Shouldn't contain nested blocks") + } + + func testParserSingleBlockToHTML() { + let parser = GutenbergContentParser(for: singleBlock) + XCTAssertEqual(parser.html(), singleBlock, "Parsed content should match the original HTML") + } + + func testParserNestedBlock() { + let parser = GutenbergContentParser(for: nestedBlock) + let blocks = parser.blocks + + let expectedParentBlockContent = """ +
+
+

Title

+ + +

This is a nested block.

+ + +
+ +

Subtitle

+ + +

This is another nested block.

+ + +

Footer

+
+
+ """ + let expectedNestedBlock1Content = """ +

This is a nested block.

+ """ + let expectedNestedBlock2Content = """ +

This is another nested block.

+ """ + + let parentBlock = blocks[0] + let nestedBlock1 = parentBlock.blocks[0] + let nestedBlock2 = parentBlock.blocks[1] + + XCTAssertEqual(blocks.count, 3, "Should return parent block and nested blocks") + XCTAssertEqual(blocks[1].content, nestedBlock1.content, "Nested block is present at root level") + XCTAssertEqual(blocks[2].content, nestedBlock2.content, "Nested block is present at root level") + + XCTAssertEqual(parentBlock.name, "wp:parent-block", "Name should match block's name") + XCTAssertEqual(parentBlock.content, expectedParentBlockContent, "Content should match block's content") + XCTAssertEqual(parentBlock.attributes.count, 1, "Attributes should contain one item") + XCTAssertEqual(parentBlock.attributes["name"] as? String, "parent", "Name attribute matches block's attribute") + XCTAssertEqual(parentBlock.blocks.count, 2, "Should contain nested blocks") + + XCTAssertEqual(nestedBlock1.name, "wp:nested-block", "Name should match block's name") + XCTAssertEqual(nestedBlock1.content, expectedNestedBlock1Content, "Content should match block's content") + XCTAssertEqual(nestedBlock1.attributes.count, 2, "Attributes should contain two items") + XCTAssertEqual(nestedBlock1.attributes["id"] as? Int, 1, "Id attribute matches block's attribute") + XCTAssertEqual(nestedBlock1.attributes["name"] as? String, "block1", "Name attribute matches block's attribute") + XCTAssertEqual(nestedBlock1.blocks.count, 0, "Shouldn't contain nested blocks") + XCTAssertEqual( + nestedBlock1.parentBlock?.content, + parentBlock.content, + "Should have a parent block and matches parent's content" + ) + + XCTAssertEqual(nestedBlock2.name, "wp:nested-block", "Name should match block's name") + XCTAssertEqual(nestedBlock2.content, expectedNestedBlock2Content, "Content should match block's content") + XCTAssertEqual(nestedBlock2.attributes.count, 2, "Attributes should contain two items") + XCTAssertEqual(nestedBlock2.attributes["id"] as? Int, 2, "Id attribute matches block's attribute") + XCTAssertEqual(nestedBlock2.attributes["name"] as? String, "block2", "Name attribute matches block's attribute") + XCTAssertEqual(nestedBlock2.blocks.count, 0, "Shouldn't contain nested blocks") + XCTAssertEqual( + nestedBlock2.parentBlock?.content, + parentBlock.content, + "Should have a parent block and matches parent's content" + ) + } + + func testParserNestedBlockToHTML() { + let parser = GutenbergContentParser(for: nestedBlock) + XCTAssertEqual(parser.html(), nestedBlock, "Parsed content should match the original HTML") + } + + func testParserModifyAttributes() { + let parser = GutenbergContentParser(for: nestedBlock) + let blocks = parser.blocks + let parentBlock = blocks[0] + parentBlock.attributes["name"] = "new-parent" + parentBlock.attributes["newId"] = 1001 + + let expectedResult = """ + +
+
+

Title

+ + +

This is a nested block.

+ + +
+ +

Subtitle

+ + +

This is another nested block.

+ + +

Footer

+
+
+ + """ + + XCTAssertEqual(parser.html(), expectedResult, "Parsed content should contain the modifications") + } + + func testParserModifyHTML() { + let parser = GutenbergContentParser(for: nestedBlock) + let blocks = parser.blocks + let parentBlock = blocks[0] + try! parentBlock.elements.select("div").first()?.addClass("new-class") + + let expectedResult = """ + +
+
+

Title

+ + +

This is a nested block.

+ + +
+ +

Subtitle

+ + +

This is another nested block.

+ + +

Footer

+
+
+ + """ + + XCTAssertEqual(parser.html(), expectedResult, "Parsed content should contain the modifications") + } + + // MARK: - Serialization contract + + func testVoidElementsAreSelfClosed() { + let input = """ + +

+ + """ + let expected = """ + +

+ + """ + XCTAssertEqual(GutenbergContentParser(for: input).html(), expected) + } + + func testEntitiesArePreserved() { + let content = """ + + Fish & chips + + """ + XCTAssertEqual(GutenbergContentParser(for: content).html(), content) + } + + func testRawTextElementsAreNotEscaped() { + // Regressing here would corrupt Custom HTML / embedded scripts. + let content = """ + + + + """ + XCTAssertTrue(GutenbergContentParser(for: content).html().contains("1 < 2 && 3 > 2")) + } + + func testPreformattedWhitespaceIsPreserved() { + let content = """ + +
line one
+              indented	tabbed
+ + """ + XCTAssertEqual(GutenbergContentParser(for: content).html(), content) + } + + func testUnicodeIsPreserved() { + let content = """ + +

café ☕ 日本語 — Alşksdf

+ + """ + XCTAssertEqual(GutenbergContentParser(for: content).html(), content) + } + + func testContentWithoutBlockCommentsIsPassedThrough() { + let content = "

hello world

" + XCTAssertEqual(GutenbergContentParser(for: content).html(), content) + } + + func testEmptyContentProducesEmptyOutput() { + XCTAssertTrue(GutenbergContentParser(for: "").html().isEmpty) + } + + func testMultipleSiblingBlocksArePreserved() { + let content = """ + +

one

+ + +

two

+ + """ + XCTAssertEqual(GutenbergContentParser(for: content).html(), content) + } + + func testHTMLIsIdempotent() { + let parser = GutenbergContentParser(for: singleBlock) + XCTAssertEqual(parser.html(), parser.html()) + } + + // MARK: - Mutation propagation (SwiftSoup 2.12+ serialization-cache regression) + + func testModifyNestedElementAttribute() throws { + // The mutated is nested inside
; unlike a top-level element, + // its change is dropped by SwiftSoup 2.12+ unless html() re-renders it. + let parser = GutenbergContentParser( + for: """ + +
+ + """ + ) + let image = try XCTUnwrap(parser.blocks.first?.elements.select("img").first()) + try image.attr("src", "https://example.com/new.jpg") + + let output = parser.html() + XCTAssertTrue(output.contains("src=\"https://example.com/new.jpg\"")) + XCTAssertFalse(output.contains("local://old.jpg")) + } + + func testModifyDeeplyNestedElement() throws { + let parser = GutenbergContentParser( + for: """ + +
+ + """ + ) + let image = try XCTUnwrap(parser.blocks.first?.elements.select("img").first()) + try image.attr("src", "https://example.com/deep.jpg") + + XCTAssertTrue(parser.html().contains("https://example.com/deep.jpg")) + } + + // MARK: - Attribute parsing + + func testMissingAttributesParseToEmptyDictionary() throws { + let parser = GutenbergContentParser( + for: """ + +
+ + """ + ) + XCTAssertTrue(try XCTUnwrap(parser.blocks.first).attributes.isEmpty) + } + + func testMalformedAttributesParseToEmptyDictionary() throws { + let parser = GutenbergContentParser( + for: """ + +
+ + """ + ) + XCTAssertTrue(try XCTUnwrap(parser.blocks.first).attributes.isEmpty) + } + + func testWrittenAttributesEscapeSlashesAndSortKeys() throws { + let parser = GutenbergContentParser( + for: """ + +
+ + """ + ) + try XCTUnwrap(parser.blocks.first).attributes = ["id": 100, "href": "https://example.com/f.pdf"] + // JSONSerialization `.sortedKeys` orders "href" before "id" and escapes slashes. + XCTAssertTrue(parser.html().contains(#"{"href":"https:\/\/example.com\/f.pdf","id":100}"#)) + } +} diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergFileUploadProcessorTests.swift b/Modules/Tests/GutenbergProcessorsTests/GutenbergFileUploadProcessorTests.swift similarity index 97% rename from Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergFileUploadProcessorTests.swift rename to Modules/Tests/GutenbergProcessorsTests/GutenbergFileUploadProcessorTests.swift index e1c06c3503ff..fe7ea472cc21 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergFileUploadProcessorTests.swift +++ b/Modules/Tests/GutenbergProcessorsTests/GutenbergFileUploadProcessorTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +@testable import GutenbergProcessors class GutenbergFileUploadProcessorTests: XCTestCase { diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergGalleryUploadProcessorTests.swift b/Modules/Tests/GutenbergProcessorsTests/GutenbergGalleryUploadProcessorTests.swift similarity index 99% rename from Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergGalleryUploadProcessorTests.swift rename to Modules/Tests/GutenbergProcessorsTests/GutenbergGalleryUploadProcessorTests.swift index 6a856b43792a..4ebda9f71df6 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergGalleryUploadProcessorTests.swift +++ b/Modules/Tests/GutenbergProcessorsTests/GutenbergGalleryUploadProcessorTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +@testable import GutenbergProcessors class GutenbergGalleryUploadProcessorTests: XCTestCase { diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergImgUploadProcessorTests.swift b/Modules/Tests/GutenbergProcessorsTests/GutenbergImgUploadProcessorTests.swift similarity index 98% rename from Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergImgUploadProcessorTests.swift rename to Modules/Tests/GutenbergProcessorsTests/GutenbergImgUploadProcessorTests.swift index bdd1f21ac9be..9474aba34642 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergImgUploadProcessorTests.swift +++ b/Modules/Tests/GutenbergProcessorsTests/GutenbergImgUploadProcessorTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +@testable import GutenbergProcessors class GutenbergImgUploadProcessorTests: XCTestCase { diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergRefactoredGalleryUploadProcessorTests.swift b/Modules/Tests/GutenbergProcessorsTests/GutenbergRefactoredGalleryUploadProcessorTests.swift similarity index 99% rename from Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergRefactoredGalleryUploadProcessorTests.swift rename to Modules/Tests/GutenbergProcessorsTests/GutenbergRefactoredGalleryUploadProcessorTests.swift index bf0b71a75857..1d2548b468cb 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergRefactoredGalleryUploadProcessorTests.swift +++ b/Modules/Tests/GutenbergProcessorsTests/GutenbergRefactoredGalleryUploadProcessorTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import WordPress +@testable import GutenbergProcessors class GutenbergRefactoredGalleryUploadProcessorTests: XCTestCase { struct ImageUploadJob { diff --git a/Package.resolved b/Package.resolved index a614aca3a293..e4f561d43c78 100644 --- a/Package.resolved +++ b/Package.resolved @@ -309,8 +309,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup", "state" : { - "revision" : "3c2c7e1e72b8abd96eafbae80323c5c1e5317437", - "version" : "2.7.5" + "revision" : "ead56133a693d0184d8c2db1a6d6394410cacfd6", + "version" : "2.13.6" } }, { diff --git a/Package.swift b/Package.swift index f9ff5b633880..6a9397e7670e 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,8 @@ let package = Package( .iOS(.v17) ], dependencies: [ - .package(path: "Modules") + .package(path: "Modules"), + .package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.13.6") ], targets: [ .testTarget( @@ -46,6 +47,15 @@ let package = Package( "WPUserAgentTests.swift" ], swiftSettings: [.swiftLanguageMode(.v5)] + ), + .testTarget( + name: "GutenbergProcessorsTests", + dependencies: [ + .product(name: "GutenbergProcessors", package: "Modules"), + .product(name: "SwiftSoup", package: "SwiftSoup") + ], + path: "Modules/Tests/GutenbergProcessorsTests", + swiftSettings: [.swiftLanguageMode(.v5)] ) ] ) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergContentParser.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergContentParser.swift deleted file mode 100644 index b5ba010d1203..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/GutenbergContentParser.swift +++ /dev/null @@ -1,191 +0,0 @@ -import XCTest -@testable import WordPress - -class GutenbergContentParserTests: XCTestCase { - let singleBlock = """ - -

Hello world!

- - """ - - let nestedBlock = """ - -
-
-

Title

- - -

This is a nested block.

- - -
- -

Subtitle

- - -

This is another nested block.

- - -

Footer

-
-
- - """ - - func testParserSingleBlock() { - let parser = GutenbergContentParser(for: singleBlock) - let blocks = parser.blocks - - let expectedBlockContent = """ -

Hello world!

- """ - - XCTAssertEqual(blocks.count, 1, "Should return one block") - - XCTAssertEqual(blocks[0].name, "wp:block", "Name should match block's name") - XCTAssertEqual(blocks[0].content, expectedBlockContent, "Content should match block's content") - XCTAssertEqual(blocks[0].attributes.count, 1, "Attributes should contain one item") - XCTAssertEqual(blocks[0].attributes["id"] as? Int, 1, "Id attribute matches block's attribute") - XCTAssertEqual(blocks[0].blocks.count, 0, "Shouldn't contain nested blocks") - } - - func testParserSingleBlockToHTML() { - let parser = GutenbergContentParser(for: singleBlock) - XCTAssertEqual(parser.html(), singleBlock, "Parsed content should match the original HTML") - } - - func testParserNestedBlock() { - let parser = GutenbergContentParser(for: nestedBlock) - let blocks = parser.blocks - - let expectedParentBlockContent = """ -
-
-

Title

- - -

This is a nested block.

- - -
- -

Subtitle

- - -

This is another nested block.

- - -

Footer

-
-
- """ - let expectedNestedBlock1Content = """ -

This is a nested block.

- """ - let expectedNestedBlock2Content = """ -

This is another nested block.

- """ - - let parentBlock = blocks[0] - let nestedBlock1 = parentBlock.blocks[0] - let nestedBlock2 = parentBlock.blocks[1] - - XCTAssertEqual(blocks.count, 3, "Should return parent block and nested blocks") - XCTAssertEqual(blocks[1].content, nestedBlock1.content, "Nested block is present at root level") - XCTAssertEqual(blocks[2].content, nestedBlock2.content, "Nested block is present at root level") - - XCTAssertEqual(parentBlock.name, "wp:parent-block", "Name should match block's name") - XCTAssertEqual(parentBlock.content, expectedParentBlockContent, "Content should match block's content") - XCTAssertEqual(parentBlock.attributes.count, 1, "Attributes should contain one item") - XCTAssertEqual(parentBlock.attributes["name"] as? String, "parent", "Name attribute matches block's attribute") - XCTAssertEqual(parentBlock.blocks.count, 2, "Should contain nested blocks") - - XCTAssertEqual(nestedBlock1.name, "wp:nested-block", "Name should match block's name") - XCTAssertEqual(nestedBlock1.content, expectedNestedBlock1Content, "Content should match block's content") - XCTAssertEqual(nestedBlock1.attributes.count, 2, "Attributes should contain two items") - XCTAssertEqual(nestedBlock1.attributes["id"] as? Int, 1, "Id attribute matches block's attribute") - XCTAssertEqual(nestedBlock1.attributes["name"] as? String, "block1", "Name attribute matches block's attribute") - XCTAssertEqual(nestedBlock1.blocks.count, 0, "Shouldn't contain nested blocks") - XCTAssertEqual(nestedBlock1.parentBlock?.content, parentBlock.content, "Should have a parent block and matches parent's content") - - XCTAssertEqual(nestedBlock2.name, "wp:nested-block", "Name should match block's name") - XCTAssertEqual(nestedBlock2.content, expectedNestedBlock2Content, "Content should match block's content") - XCTAssertEqual(nestedBlock2.attributes.count, 2, "Attributes should contain two items") - XCTAssertEqual(nestedBlock2.attributes["id"] as? Int, 2, "Id attribute matches block's attribute") - XCTAssertEqual(nestedBlock2.attributes["name"] as? String, "block2", "Name attribute matches block's attribute") - XCTAssertEqual(nestedBlock2.blocks.count, 0, "Shouldn't contain nested blocks") - XCTAssertEqual(nestedBlock2.parentBlock?.content, parentBlock.content, "Should have a parent block and matches parent's content") - } - - func testParserNestedBlockToHTML() { - let parser = GutenbergContentParser(for: nestedBlock) - XCTAssertEqual(parser.html(), nestedBlock, "Parsed content should match the original HTML") - } - - func testParserModifyAttributes() { - let parser = GutenbergContentParser(for: nestedBlock) - let blocks = parser.blocks - let parentBlock = blocks[0] - parentBlock.attributes["name"] = "new-parent" - parentBlock.attributes["newId"] = 1001 - - let expectedResult = """ - -
-
-

Title

- - -

This is a nested block.

- - -
- -

Subtitle

- - -

This is another nested block.

- - -

Footer

-
-
- - """ - - XCTAssertEqual(parser.html(), expectedResult, "Parsed content should contain the modifications") - } - - func testParserModifyHTML() { - let parser = GutenbergContentParser(for: nestedBlock) - let blocks = parser.blocks - let parentBlock = blocks[0] - try! parentBlock.elements.select("div").first()?.addClass("new-class") - - let expectedResult = """ - -
-
-

Title

- - -

This is a nested block.

- - -
- -

Subtitle

- - -

This is another nested block.

- - -

Footer

-
-
- - """ - - XCTAssertEqual(parser.html(), expectedResult, "Parsed content should contain the modifications") - } -} diff --git a/Tests/KeystoneTests/WordPressUnitTests.xctestplan b/Tests/KeystoneTests/WordPressUnitTests.xctestplan index f172c2102e2f..68e2e6e2e24f 100644 --- a/Tests/KeystoneTests/WordPressUnitTests.xctestplan +++ b/Tests/KeystoneTests/WordPressUnitTests.xctestplan @@ -41,6 +41,13 @@ "name" : "DesignSystemTests" } }, + { + "target" : { + "containerPath" : "container:..\/Modules", + "identifier" : "GutenbergProcessorsTests", + "name" : "GutenbergProcessorsTests" + } + }, { "target" : { "containerPath" : "container:..\/Modules", diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index b464bec59047..924a3ff936f2 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -3,6 +3,7 @@ import Aztec import AztecExtensions import Combine import Foundation +import GutenbergProcessors import WordPressData import WordPressKit import WordPressFlux