Skip to content

Commit 97c18da

Browse files
authored
Update SwiftSoup from 2.7.5 to 2.13.6 (#25811)
* Update `SwiftSoup` from `2.7.5` to `2.13.6` Removes the `'swiftsoup': found 4 file(s) which are unhandled` warning emitted during `swift test`. Upstream dropped the four stray `Info*.plist` files from the package's `Sources/` directory in 2.8.2, so any release past our pin resolves it — this takes us to the latest, 2.13.6. No breaking API changes across 2.8–2.13.6 (Swift 5.9+ floor, satisfied). Our usage — `parse`, `parseBodyFragment`, `clean`, `Element`/`Elements`/`Comment`/ `Entities` — is unchanged. * Fix Gutenberg upload processors under `SwiftSoup` 2.13.6 `SwiftSoup` 2.12+ serializes unchanged nodes from a cached slice of the original source, so the `attr()` mutations the media upload processors make on nested elements weren't reflected in the output — only the mutated node is marked dirty, not its ancestors, so `body.html()` re-emitted stale bytes and uploaded media kept their local `file://` URLs and temp IDs. `GutenbergContentParser.html()` now replaces each top-level element with a copy to force a fresh render of the mutated subtrees, while emitting the surrounding comment/text nodes (Gutenberg block delimiters) from their original bytes. Output is byte-for-byte identical to the previous behavior. All Gutenberg upload processor tests pass on 2.13.6. * Add `GutenbergContentParser` tests for its serialization contract Extends the existing suite with the coverage the SwiftSoup 2.13.6 fix relies on but that was missing. The prior element-mutation test only mutates a top-level node, whose change survives serialization even with the 2.12+ regression; the new tests mutate nested and deeply-nested elements, which are the paths that actually broke. Also pins the serialization contract that shifts across SwiftSoup versions — void-element normalization, entity / raw-text / preformatted / unicode fidelity, `html()` idempotence — and the malformed-JSON and sorted-key attribute handling. * Extract Gutenberg content processors into a `GutenbergProcessors` module Moves the SwiftSoup-based Gutenberg code — `GutenbergContentParser`, the `GutenbergProcessor` protocol, and the image/gallery/file upload processors — out of the app target into a new `GutenbergProcessors` module so its tests run under `swift test` on macOS instead of a full iOS app build. The Aztec-based processors stay in the app; they depend on UIKit and can't run cross-platform. Wiring is entirely in Package.swift, no project changes: the module is added to `keystoneDependencies` (how the app links it), a cross-platform test target is added to the root package, and the target is registered in the unit-test plan. `PostCoordinator` is the only consumer. Runs via `swift test --filter GutenbergProcessorsTests`.
1 parent 227641a commit 97c18da

17 files changed

Lines changed: 452 additions & 242 deletions

Modules/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let package = Package(
1212
.library(name: "AsyncImageKit", targets: ["AsyncImageKit"]),
1313
.library(name: "DesignSystem", targets: ["DesignSystem"]),
1414
.library(name: "FormattableContentKit", targets: ["FormattableContentKit"]),
15+
.library(name: "GutenbergProcessors", targets: ["GutenbergProcessors"]),
1516
.library(name: "JetpackStats", targets: ["JetpackStats"]),
1617
.library(name: "JetpackSocial", targets: ["JetpackSocial"]),
1718
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
@@ -46,7 +47,7 @@ let package = Package(
4647
.package(url: "https://github.com/erikdoe/ocmock", revision: "2c0bfd373289f4a7716db5d6db471640f91a6507"),
4748
.package(url: "https://github.com/johnxnguyen/Down", branch: "master"),
4849
.package(url: "https://github.com/kaishin/Gifu", from: "3.4.1"),
49-
.package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.7.5"),
50+
.package(url: "https://github.com/scinfu/SwiftSoup", exact: "2.13.6"),
5051
.package(url: "https://github.com/squarefrog/UIDeviceIdentifier", from: "2.3.0"),
5152
// We can remove the SVProgressHUD fork once this PR is merged: https://github.com/SVProgressHUD/SVProgressHUD/pull/1131
5253
.package(url: "https://github.com/automattic/SVProgressHUD", branch: "master"),
@@ -334,6 +335,11 @@ let package = Package(
334335
.enableUpcomingFeature("BareSlashRegexLiterals")
335336
]
336337
),
338+
.target(
339+
name: "GutenbergProcessors",
340+
dependencies: [.product(name: "SwiftSoup", package: "SwiftSoup")],
341+
swiftSettings: [.swiftLanguageMode(.v5)]
342+
),
337343
.target(
338344
name: "WordPressReader",
339345
dependencies: [
@@ -400,6 +406,14 @@ let package = Package(
400406
),
401407
.testTarget(name: "WordPressCoreTests", dependencies: [.target(name: "WordPressCore")]),
402408
.testTarget(name: "WordPressIntelligenceTests", dependencies: [.target(name: "WordPressIntelligence")]),
409+
.testTarget(
410+
name: "GutenbergProcessorsTests",
411+
dependencies: [
412+
.target(name: "GutenbergProcessors"),
413+
.product(name: "SwiftSoup", package: "SwiftSoup")
414+
],
415+
swiftSettings: [.swiftLanguageMode(.v5)]
416+
),
403417
.testTarget(name: "WordPressReaderTests", dependencies: [.target(name: "WordPressReader")]),
404418
.testTarget(
405419
name: "JetpackSocialTests",
@@ -484,6 +498,7 @@ enum XcodeSupport {
484498
"DesignSystem",
485499
"BuildSettingsKit",
486500
"FormattableContentKit",
501+
"GutenbergProcessors",
487502
"JetpackSocial",
488503
"JetpackStats",
489504
"JetpackStatsWidgetsCore",

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergContentParser.swift renamed to Modules/Sources/GutenbergProcessors/GutenbergContentParser.swift

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ public class GutenbergParsedBlock {
1010

1111
public var attributes: [String: Any] {
1212
get {
13-
guard let data = self.attributesData.data(using: .utf8 ),
14-
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
15-
let attributes = jsonObject as? [String: Any]
13+
guard let data = self.attributesData.data(using: .utf8),
14+
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
15+
let attributes = jsonObject as? [String: Any]
1616
else {
1717
return [:]
1818
}
@@ -21,7 +21,8 @@ public class GutenbergParsedBlock {
2121

2222
set(newValue) {
2323
guard let data = try? JSONSerialization.data(withJSONObject: newValue, options: .sortedKeys),
24-
let attributes = String(data: data, encoding: .utf8) else {
24+
let attributes = String(data: data, encoding: .utf8)
25+
else {
2526
return
2627
}
2728
self.attributesData = attributes
@@ -44,8 +45,7 @@ public class GutenbergParsedBlock {
4445
if let separatorRange = data.range(of: " ") {
4546
self.name = String(data[data.startIndex..<separatorRange.lowerBound])
4647
self.attributesData = String(data[separatorRange.upperBound..<data.endIndex])
47-
}
48-
else {
48+
} else {
4949
self.name = data
5050
self.attributesData = ""
5151
}
@@ -114,7 +114,8 @@ public class GutenbergContentParser {
114114
private let htmlDocument: Document?
115115

116116
public init(for content: String) {
117-
self.htmlDocument = try? SwiftSoup.parseBodyFragment(content).outputSettings(OutputSettings().prettyPrint(pretty: false))
117+
self.htmlDocument = try? SwiftSoup.parseBodyFragment(content)
118+
.outputSettings(OutputSettings().prettyPrint(pretty: false))
118119
self.blocks = []
119120

120121
guard let htmlContent = self.htmlDocument?.body() else {
@@ -124,37 +125,53 @@ public class GutenbergContentParser {
124125
}
125126

126127
public func html() -> String {
127-
return (try? self.htmlDocument?.body()?.html()) ?? ""
128+
guard let body = self.htmlDocument?.body() else {
129+
return ""
130+
}
131+
// SwiftSoup 2.12+ serializes unchanged nodes from a cached copy of the
132+
// original source, so the attribute mutations the processors make on
133+
// nested elements aren't reflected in the output. Replacing each
134+
// top-level element with a copy marks its subtree dirty and forces a
135+
// re-render, while the surrounding comment and text nodes (the Gutenberg
136+
// block delimiters) are emitted from their original bytes.
137+
for element in body.children().array() {
138+
guard let clone = try? element.copy() as? Element else {
139+
continue
140+
}
141+
try? element.replaceWith(clone)
142+
}
143+
return (try? body.html()) ?? ""
128144
}
129145

130146
private func traverseChildNodes(element: Element, parentBlock: GutenbergParsedBlock? = nil) {
131147
var currentBlock: GutenbergParsedBlock?
132-
element.getChildNodes().forEach { node in
133-
switch node {
134-
// Convert comment tag into block
135-
case let comment as SwiftSoup.Comment:
136-
guard let block = GutenbergParsedBlock(comment: comment, parentBlock: parentBlock) else {
137-
return
138-
}
148+
element.getChildNodes()
149+
.forEach { node in
150+
switch node {
151+
// Convert comment tag into block
152+
case let comment as SwiftSoup.Comment:
153+
guard let block = GutenbergParsedBlock(comment: comment, parentBlock: parentBlock) else {
154+
return
155+
}
139156

140-
// Identify close tag
141-
if let currrentBlock = currentBlock, block.name == "/\(currrentBlock.name)" {
142-
currentBlock = nil
143-
return
144-
}
157+
// Identify close tag
158+
if let currrentBlock = currentBlock, block.name == "/\(currrentBlock.name)" {
159+
currentBlock = nil
160+
return
161+
}
145162

146-
self.blocks.append(block)
147-
currentBlock = block
148-
// Insert HTML elements into block being processed
149-
case let element as SwiftSoup.Element:
150-
if let currentBlock {
151-
currentBlock.elements.add(element)
163+
self.blocks.append(block)
164+
currentBlock = block
165+
// Insert HTML elements into block being processed
166+
case let element as SwiftSoup.Element:
167+
if let currentBlock {
168+
currentBlock.elements.add(element)
169+
}
170+
if element.childNodeSize() > 0 {
171+
traverseChildNodes(element: element, parentBlock: currentBlock ?? parentBlock)
172+
}
173+
default: break
152174
}
153-
if element.childNodeSize() > 0 {
154-
traverseChildNodes(element: element, parentBlock: currentBlock ?? parentBlock)
155-
}
156-
default: break
157175
}
158-
}
159176
}
160177
}

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift renamed to Modules/Sources/GutenbergProcessors/GutenbergFileUploadProcessor.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
class GutenbergFileUploadProcessor: GutenbergProcessor {
3+
public class GutenbergFileUploadProcessor: GutenbergProcessor {
44
private struct FileBlockKeys {
55
static var name = "wp:file"
66
static var id = "id"
@@ -11,7 +11,7 @@ class GutenbergFileUploadProcessor: GutenbergProcessor {
1111
let remoteURLString: String
1212
let serverMediaID: Int
1313

14-
init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) {
14+
public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) {
1515
self.mediaUploadID = mediaUploadID
1616
self.serverMediaID = serverMediaID
1717
self.remoteURLString = remoteURLString
@@ -34,7 +34,7 @@ class GutenbergFileUploadProcessor: GutenbergProcessor {
3434
}
3535
}
3636

37-
func process(_ blocks: [GutenbergParsedBlock]) {
37+
public func process(_ blocks: [GutenbergParsedBlock]) {
3838
processFileBlocks(blocks)
3939
}
4040
}

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift renamed to Modules/Sources/GutenbergProcessors/GutenbergGalleryUploadProcessor.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import SwiftSoup
33

4-
class GutenbergGalleryUploadProcessor: GutenbergProcessor {
4+
public class GutenbergGalleryUploadProcessor: GutenbergProcessor {
55

66
let mediaUploadID: Int32
77
let remoteURLString: String
@@ -12,7 +12,7 @@ class GutenbergGalleryUploadProcessor: GutenbergProcessor {
1212

1313
static let imgClassIDPrefixAttribute = "wp-image-"
1414

15-
init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String, mediaLink: String) {
15+
public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String, mediaLink: String) {
1616
self.mediaUploadID = mediaUploadID
1717
self.serverMediaID = serverMediaID
1818
self.remoteURLString = remoteURLString
@@ -139,7 +139,7 @@ class GutenbergGalleryUploadProcessor: GutenbergProcessor {
139139
}
140140
}
141141

142-
func process(_ blocks: [GutenbergParsedBlock]) {
142+
public func process(_ blocks: [GutenbergParsedBlock]) {
143143
processGalleryBlocks(blocks)
144144
}
145145
}

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift renamed to Modules/Sources/GutenbergProcessors/GutenbergImgUploadProcessor.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import Foundation
22

3-
class GutenbergImgUploadProcessor: GutenbergProcessor {
3+
public class GutenbergImgUploadProcessor: GutenbergProcessor {
44

55
let mediaUploadID: Int32
66
let remoteURLString: String
77
let serverMediaID: Int
88
static let imgClassIDPrefixAttribute = "wp-image-"
99

10-
init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) {
10+
public init(mediaUploadID: Int32, serverMediaID: Int, remoteURLString: String) {
1111
self.mediaUploadID = mediaUploadID
1212
self.serverMediaID = serverMediaID
1313
self.remoteURLString = remoteURLString
@@ -63,7 +63,7 @@ class GutenbergImgUploadProcessor: GutenbergProcessor {
6363
}
6464
}
6565

66-
func process(_ blocks: [GutenbergParsedBlock]) {
66+
public func process(_ blocks: [GutenbergParsedBlock]) {
6767
processImageBlocks(blocks)
6868
processMediaTextBlocks(blocks)
6969
}

WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergProcessor.swift renamed to Modules/Sources/GutenbergProcessors/GutenbergProcessor.swift

File renamed without changes.

0 commit comments

Comments
 (0)