Skip to content

Commit c0de3ec

Browse files
committed
Decouple swiftsoup from the library
1 parent 45713f4 commit c0de3ec

13 files changed

+312
-123
lines changed

Package.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,35 @@ let package = Package(
1515
targets: [
1616
.target(
1717
name: "GutenbergKit",
18-
dependencies: ["SwiftSoup"],
18+
dependencies: [],
1919
path: "ios/Sources/GutenbergKit",
2020
exclude: [],
2121
resources: [.copy("Gutenberg")]
2222
),
23+
.target(
24+
name: "SwiftSoupAssetManifestParser",
25+
dependencies: ["GutenbergKit", "SwiftSoup"],
26+
path: "ios/Sources/SwiftSoupAssetManifestParser",
27+
exclude: [],
28+
resources: []
29+
),
2330
.testTarget(
2431
name: "GutenbergKitTests",
2532
dependencies: ["GutenbergKit"],
26-
path: "ios/Tests",
33+
path: "ios/Tests/GutenbergKitTests",
2734
exclude: [],
2835
resources: [
29-
.copy("GutenbergKitTests/Resources/manifest-test-case-1.json")
36+
.copy("Resources/manifest-test-case-1.json")
3037
]
3138
),
39+
.testTarget(
40+
name: "SwiftSoupAssetManifestParserTests",
41+
dependencies: ["SwiftSoupAssetManifestParser"],
42+
path: "ios/Tests/SwiftSoupAssetManifestParserTests",
43+
exclude: [],
44+
resources: [
45+
.copy("../GutenbergKitTests/Resources/manifest-test-case-1.json")
46+
]
47+
)
3248
]
3349
)

ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
1616
return components.url
1717
}
1818

19-
nonisolated static func cachedURL(forWebLink link: String) -> String? {
20-
if link.starts(with: "http://") || link.starts(with: "https://") {
21-
return cachedURLSchemePrefix + link
19+
nonisolated static func cachedURL(for url: URL) -> URL {
20+
if url.scheme == "http" || url.scheme == "https" {
21+
var components = URLComponents(string: url.absoluteString)!
22+
components.scheme = cachedURLSchemePrefix + (url.scheme ?? "")
23+
return components.url!
2224
}
23-
return nil
25+
26+
return url
2427
}
2528

2629
let worker: Worker
@@ -105,4 +108,3 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
105108
}
106109
}
107110
}
108-

ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift

Lines changed: 99 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import Foundation
22
import CryptoKit
3-
import SwiftSoup
43

54
public actor EditorAssetsLibrary {
65
enum ManifestError: Error {
76
case unavailable
87
case invalidServerResponse
8+
case invalidSiteUrl
99
}
1010

1111
let urlSession: URLSession
@@ -45,46 +45,46 @@ public actor EditorAssetsLibrary {
4545
///
4646
/// - SeeAlso: `CachedAssetSchemeHandler`
4747
/// - SeeAlso: `EditorAssetsLibrary.addAsset`
48-
func manifestContentForEditor() async throws -> Data {
48+
func manifestContentForEditor(using parser: EditorAssetManifestParser) async throws -> Data {
4949
// For scheme-less links (i.e. '//stats.wp.com/w.js'), use the scheme in `siteURL`.
50-
let siteURLScheme = URL(string: configuration.siteURL)?.scheme
50+
guard let siteURLScheme = URL(string: configuration.siteURL)?.scheme else {
51+
throw ManifestError.invalidSiteUrl
52+
}
53+
5154
let data = try await loadManifestContent()
52-
let manifest = try JSONDecoder().decode(EditorAssetsMainifest.self, from: data)
53-
return try manifest.renderForEditor(defaultScheme: siteURLScheme)
55+
let manifest = try EditorAssetManifest(data: data)
56+
57+
return try JSONEncoder().encode(manifest.applyingUrlScheme(siteURLScheme, using: parser))
5458
}
5559

5660
/// Fetches all assets in the `EditorConfiguration.editorAssetsEndpoint` manifest and stores them on the device.
5761
///
5862
/// - SeeAlso: CachedAssetSchemeHandler
59-
public func fetchAssets() async throws {
63+
public func fetchAssets(using manifestParser: EditorAssetManifestParser) async throws {
6064
// For scheme-less links (i.e. '//stats.wp.com/w.js'), use the scheme in `siteURL`.
6165
let siteURLScheme = URL(string: configuration.siteURL)?.scheme
6266

6367
let data = try await loadManifestContent()
64-
let manifest = try JSONDecoder().decode(EditorAssetsMainifest.self, from: data)
65-
let assetLinks = try manifest.parseAssetLinks(defaultScheme: siteURLScheme)
66-
67-
for link in assetLinks {
68-
guard let url = URL(string: link) else {
69-
NSLog("Malformed asset link: \(link)")
70-
continue
71-
}
68+
let manifest = try EditorAssetManifest(data: data).applyingUrlScheme(siteURLScheme, using: manifestParser)
69+
let assetUrls = try manifest.getAllAssetUrls(using: manifestParser)
7270

71+
for url in assetUrls {
7372
guard url.scheme == "http" || url.scheme == "https" else {
74-
NSLog("Unexpected asset link: \(link)")
73+
NSLog("Unexpected asset link: \(url)")
7574
continue
7675
}
7776

78-
_ = try await cacheAsset(from: url)
77+
try await cacheAsset(from: url)
7978
}
80-
NSLog("\(assetLinks.count) resources processed.")
79+
NSLog("\(assetUrls.count) resources processed.")
8180
}
8281

8382
/// Fetches one asset (JavaScript or stylesheet) and caches its content on the device.
8483
///
8584
/// - Parameters:
8685
/// - httpURL: The javascript or css URL.
8786
/// - webViewURL: The corresponding URL requested by web view, which should the "GBK cache prefix" (`gbk-cache-https://`)
87+
@discardableResult
8888
func cacheAsset(from httpURL: URL, webViewURL: URL? = nil) async throws -> (URLResponse, Data) {
8989
// The Web Inspector automatically requests ".js.map" files, we'll support it here for debugging purpose.
9090
let supportedResourceSuffixes = [".js", ".css", ".js.map"]
@@ -191,97 +191,107 @@ private extension String {
191191
}
192192
}
193193

194-
struct EditorAssetsMainifest: Codable {
195-
var scripts: String
196-
var styles: String
197-
var allowedBlockTypes: [String]
194+
public struct EditorAssetSchemeResolver {
195+
196+
// Takes a URL string and applies the given scheme to it.
197+
//
198+
// If there is no scheme present, the `defaultScheme` will be applied to it. If no `defaultScheme` is
199+
// provided, `https` will be used.
200+
public static func resolveSchemeFor(_ link: String, defaultScheme: String?) -> String {
201+
if link.starts(with: "//") {
202+
return "\(defaultScheme ?? "https"):\(link)"
203+
}
204+
205+
return link
206+
}
207+
}
208+
209+
210+
// An object representing the JSON response we receive from the server
211+
//
212+
public struct EditorAssetManifest: Codable {
213+
public let scripts: String
214+
public let styles: String
215+
public let allowedBlockTypes: [String]
198216

199217
enum CodingKeys: String, CodingKey {
200218
case scripts
201219
case styles
202220
case allowedBlockTypes = "allowed_block_types"
203221
}
204222

205-
func parseAssetLinks(defaultScheme: String?) throws -> [String] {
206-
let html = """
207-
<html>
208-
<head>
209-
\(scripts)
210-
\(styles)
211-
</head>
212-
<body></body>
213-
</html>
214-
"""
215-
let document = try SwiftSoup.parse(html)
216-
217-
var assetLinks: [String] = []
218-
assetLinks += try document.select("script[src]").map {
219-
Self.resolveAssetLink(try $0.attr("src"), defaultScheme: defaultScheme)
220-
}
221-
assetLinks += try document.select(#"link[rel="stylesheet"][href]"#).map {
222-
Self.resolveAssetLink(try $0.attr("href"), defaultScheme: defaultScheme)
223-
}
224-
return assetLinks
223+
init(data: Data) throws {
224+
self = try JSONDecoder().decode(EditorAssetManifest.self, from: data)
225225
}
226226

227-
func renderForEditor(defaultScheme: String?) throws -> Data {
228-
var rendered = self
229-
rendered.scripts = try Self.renderForEditor(scripts: self.scripts, defaultScheme: defaultScheme)
230-
rendered.styles = try Self.renderForEditor(styles: self.styles, defaultScheme: defaultScheme)
231-
return try JSONEncoder().encode(rendered)
227+
init(scripts: String, styles: String, allowedBlockTypes: [String]) {
228+
self.scripts = scripts
229+
self.styles = styles
230+
self.allowedBlockTypes = allowedBlockTypes
232231
}
233232

234-
private static func renderForEditor(scripts: String, defaultScheme: String?) throws -> String {
235-
let html = """
236-
<html>
237-
<head>
238-
\(scripts)
239-
</head>
240-
<body></body>
241-
</html>
242-
"""
243-
let document = try SwiftSoup.parse(html)
244-
245-
for script in try document.select("script[src]") {
246-
if let src = try? script.attr("src") {
247-
let link = Self.resolveAssetLink(src, defaultScheme: defaultScheme)
248-
let newLink = CachedAssetSchemeHandler.cachedURL(forWebLink: link) ?? link
249-
try script.attr("src", newLink)
250-
}
251-
}
233+
func getScriptUrlStrings(using parser: EditorAssetManifestParser) throws -> [String] {
234+
try parser.extractScriptURLs(from: self.scripts)
235+
}
252236

253-
let head = document.head()!
254-
return try head.html()
237+
func getScriptUrls(using parser: EditorAssetManifestParser) throws -> [URL] {
238+
try getScriptUrlStrings(using: parser).compactMap(URL.init)
255239
}
256240

257-
private static func renderForEditor(styles: String, defaultScheme: String?) throws -> String {
258-
let html = """
259-
<html>
260-
<head>
261-
\(styles)
262-
</head>
263-
<body></body>
264-
</html>
265-
"""
266-
let document = try SwiftSoup.parse(html)
267-
268-
for stylesheet in try document.select(#"link[rel="stylesheet"][href]"#) {
269-
if let href = try? stylesheet.attr("href") {
270-
let link = Self.resolveAssetLink(href, defaultScheme: defaultScheme)
271-
let newLink = CachedAssetSchemeHandler.cachedURL(forWebLink: link) ?? link
272-
try stylesheet.attr("href", newLink)
273-
}
241+
func getStyleUrlStrings(using parser: EditorAssetManifestParser) throws -> [String] {
242+
try parser.extractStyleURLs(from: self.styles)
243+
}
244+
245+
func getStyleUrls(using parser: EditorAssetManifestParser) throws -> [URL] {
246+
try getStyleUrlStrings(using: parser).compactMap(URL.init)
247+
}
248+
249+
func getAllAssetUrls(applyingDefaultScheme scheme: String? = nil, using parser: EditorAssetManifestParser) throws -> [URL] {
250+
let scriptUrls = try self.getScriptUrls(using: parser)
251+
let styleUrls = try self.getStyleUrls(using: parser)
252+
253+
return scriptUrls + styleUrls
254+
}
255+
256+
func applyingUrlScheme(_ newScheme: String?, using manifestParser: EditorAssetManifestParser) throws -> Self {
257+
var mutableStyles = self.styles
258+
var mutableScripts = self.scripts
259+
260+
for rawLink in try getStyleUrlStrings(using: manifestParser) {
261+
let resolvedLink = EditorAssetSchemeResolver.resolveSchemeFor(rawLink, defaultScheme: newScheme)
262+
mutableStyles = mutableStyles.replacingOccurrences(of: rawLink, with: resolvedLink)
263+
}
264+
265+
for rawLink in try getScriptUrlStrings(using: manifestParser) {
266+
let resolvedLink = EditorAssetSchemeResolver.resolveSchemeFor(rawLink, defaultScheme: newScheme)
267+
mutableScripts = mutableScripts.replacingOccurrences(of: rawLink, with: resolvedLink)
274268
}
275269

276-
let head = document.head()!
277-
return try head.html()
270+
return EditorAssetManifest(
271+
scripts: mutableScripts,
272+
styles: mutableStyles,
273+
allowedBlockTypes: self.allowedBlockTypes
274+
)
278275
}
279276

280-
private static func resolveAssetLink(_ link: String, defaultScheme: String?) -> String {
281-
if link.starts(with: "//") {
282-
return "\(defaultScheme ?? "https"):\(link)"
277+
func resolvingCachedUrls(using manifestParser: EditorAssetManifestParser) throws -> Self {
278+
var mutableStyles = self.styles
279+
var mutableScripts = self.scripts
280+
281+
for url in try getStyleUrls(using: manifestParser) {
282+
let cachedLink = CachedAssetSchemeHandler.cachedURL(for: url)
283+
mutableStyles = mutableStyles.replacingOccurrences(of: url.absoluteString, with: cachedLink.absoluteString)
283284
}
284285

285-
return link
286+
for url in try getScriptUrls(using: manifestParser) {
287+
let cachedLink = CachedAssetSchemeHandler.cachedURL(for: url)
288+
mutableScripts = mutableScripts.replacingOccurrences(of: url.absoluteString, with: cachedLink.absoluteString)
289+
}
290+
291+
return EditorAssetManifest(
292+
scripts: mutableScripts,
293+
styles: mutableStyles,
294+
allowedBlockTypes: self.allowedBlockTypes
295+
)
286296
}
287297
}

ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsProvider.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import WebKit
33

44
class EditorAssetsProvider: NSObject, WKScriptMessageHandlerWithReply {
55
let library: EditorAssetsLibrary
6+
let manifestParser: EditorAssetManifestParser
67

7-
init(library: EditorAssetsLibrary) {
8+
init(library: EditorAssetsLibrary, manifestParser: EditorAssetManifestParser) {
89
self.library = library
10+
self.manifestParser = manifestParser
911
}
1012

1113
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping @MainActor @Sendable (Any?, String?) -> Void) {
@@ -19,7 +21,7 @@ class EditorAssetsProvider: NSObject, WKScriptMessageHandlerWithReply {
1921

2022
Task.detached { [library] in
2123
do {
22-
let data = try await library.manifestContentForEditor()
24+
let data = try await library.manifestContentForEditor(using: self.manifestParser)
2325
let dict = try JSONSerialization.jsonObject(with: data)
2426
await replyHandler(dict, nil)
2527
} catch {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
import OSLog
3+
4+
extension EditorViewController {
5+
func getAssetManifestParser() -> EditorAssetManifestParser {
6+
if let parser = self.assetManifestParser {
7+
return parser
8+
}
9+
10+
if #available(iOS 16.0, *) {
11+
Logger.gbkit.warning("Warning: using the default `AssetManifestParser` – this is not recommended. You can use the included `SwiftSoupAssetManifestParser` or create your own.")
12+
return DefaultEditorAssetManifestParser()
13+
} else {
14+
preconditionFailure("You must provide an implementation of `EditorAssetManifestParser`. You can use the included `SwiftSoupAssetManifestParser` or create your own.")
15+
}
16+
}
17+
}

ios/Sources/GutenbergKit/Sources/EditorViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
1919

2020
public weak var delegate: EditorViewControllerDelegate?
2121

22+
public var assetManifestParser: EditorAssetManifestParser?
23+
2224
private var cancellables: [AnyCancellable] = []
2325

2426
/// Warmup mode preloads resources into memory to make the UI transition seamless when displaying the editor for the first time
@@ -123,7 +125,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
123125
private func loadEditor() {
124126
if configuration.plugins {
125127
webView.configuration.userContentController.addScriptMessageHandler(
126-
EditorAssetsProvider(library: assetsLibrary),
128+
EditorAssetsProvider(library: assetsLibrary, manifestParser: self.getAssetManifestParser()),
127129
contentWorld: .page,
128130
name: "loadFetchedEditorAssets"
129131
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
#if canImport(OSLog)
4+
import OSLog
5+
6+
extension Logger {
7+
static let gbkit = Logger(subsystem: "org.wordpress.gutenberg", category: "all")
8+
}
9+
#endif

0 commit comments

Comments
 (0)