Skip to content

Commit 17e0732

Browse files
committed
Add EditorService
1 parent 0b7f3ff commit 17e0732

File tree

3 files changed

+166
-1
lines changed

3 files changed

+166
-1
lines changed

ios/Demo-iOS/Sources/Views/AppRootView.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct AppRootView: View {
7272
let canUsePlugins = apiRoot.hasRoute(route: "/wpcom/v2/editor-assets")
7373
let canUseEditorStyles = apiRoot.hasRoute(route: "/wp-block-editor/v1/settings")
7474

75-
let updatedConfiguration = EditorConfigurationBuilder()
75+
var updatedConfiguration = EditorConfigurationBuilder()
7676
.setShouldUseThemeStyles(canUseEditorStyles)
7777
.setShouldUsePlugins(canUsePlugins)
7878
.setSiteUrl(config.siteUrl)
@@ -82,6 +82,19 @@ struct AppRootView: View {
8282
.setLogLevel(.debug)
8383
.build()
8484

85+
if let baseURL = URL(string: config.siteApiRoot) {
86+
let service = EditorService(
87+
siteID: config.siteUrl,
88+
baseURL: baseURL,
89+
authHeader: config.authHeader
90+
)
91+
do {
92+
try await service.setup(&updatedConfiguration)
93+
} catch {
94+
print("Failed to setup editor environment, confinuing with the default or cached configuration:", error)
95+
}
96+
}
97+
8598
self.activeEditorConfiguration = updatedConfiguration
8699
} catch {
87100
self.hasError = true
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
extension String {
4+
/// Converts a string (such as a URL) into a safe directory name by removing illegal filesystem characters
5+
///
6+
/// This method filters out characters that are not allowed in directory names across different filesystems,
7+
/// including: `/`, `:`, `\`, `?`, `%`, `*`, `|`, `"`, `<`, `>`, newlines, and control characters.
8+
///
9+
/// Example:
10+
/// ```swift
11+
/// let url = "https://example.com/path?query=1"
12+
/// let safeName = url.safeFilename
13+
/// // Result: "https---example.com-path-query-1"
14+
/// ```
15+
var safeFilename: String {
16+
// Define illegal characters for directory names
17+
let illegalChars = CharacterSet(charactersIn: "/:\\?%*|\"<>")
18+
.union(.newlines)
19+
.union(.controlCharacters)
20+
21+
// Remove scheme and other URL components we don't want
22+
var cleaned = self
23+
if var urlComponents = URLComponents(string: self) {
24+
urlComponents.scheme = nil
25+
urlComponents.query = nil
26+
urlComponents.fragment = nil
27+
if let url = urlComponents.url?.absoluteString {
28+
cleaned = url
29+
}
30+
}
31+
32+
// Trim and replace illegal characters with dashes
33+
return cleaned
34+
.trimmingCharacters(in: illegalChars)
35+
.components(separatedBy: illegalChars)
36+
.joined(separator: "-")
37+
}
38+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import Foundation
2+
3+
/// Service for fetching the editor settings and other parts of the enrvironment
4+
/// required to launch the editor.
5+
public final class EditorService {
6+
enum EditorServiceError: Error {
7+
case invalidResponseData
8+
}
9+
10+
private let siteID: String
11+
private let baseURL: URL
12+
private let authHeader: String
13+
private let urlSession: URLSession
14+
15+
private let storeURL: URL
16+
private var editorSettingsFileURL: URL { storeURL.appendingPathComponent("settings.json") }
17+
18+
private var refreshTask: Task<Void, Error>?
19+
20+
/// Creates a new EditorService instance
21+
/// - Parameters:
22+
/// - siteID: Unique identifier for the site (used for caching)
23+
/// - baseURL: Root URL for the site API
24+
/// - authHeader: Authorization header value
25+
/// - urlSession: URLSession to use for network requests (defaults to .shared)
26+
public init(siteID: String, baseURL: URL, authHeader: String, urlSession: URLSession = .shared) {
27+
self.siteID = siteID
28+
self.baseURL = baseURL
29+
self.authHeader = authHeader
30+
self.urlSession = urlSession
31+
32+
self.storeURL = URL.documentsDirectory
33+
.appendingPathComponent("GutenbergKit", isDirectory: true)
34+
.appendingPathComponent(siteID.safeFilename, isDirectory: true)
35+
}
36+
37+
/// Set up the editor for the given site.
38+
///
39+
/// - warning: The request make take a significant amount of time the first
40+
/// time you open the editor.
41+
public func setup(_ configuration: inout EditorConfiguration) async throws {
42+
var builder = configuration.toBuilder()
43+
44+
if !isEditorLoaded {
45+
try await refresh()
46+
}
47+
48+
if let data = try? Data(contentsOf: editorSettingsFileURL),
49+
let settings = String(data: data, encoding: .utf8) {
50+
builder = builder.setEditorSettings(settings)
51+
}
52+
53+
return configuration = builder.build()
54+
}
55+
56+
/// Returns `true` is the resources requied for the editor already exist.
57+
private var isEditorLoaded: Bool {
58+
FileManager.default.fileExists(atPath: editorSettingsFileURL.absoluteString)
59+
}
60+
61+
/// Refresh the editor settings and other resources.
62+
public func refresh() async throws {
63+
if let task = refreshTask {
64+
return try await task.value
65+
}
66+
let task = Task {
67+
defer { refreshTask = nil }
68+
try await actuallyRefresh()
69+
}
70+
refreshTask = task
71+
return try await task.value
72+
}
73+
74+
private func actuallyRefresh() async throws {
75+
try await fetchEditorSettings()
76+
}
77+
78+
// MARK: – Editor Settings
79+
80+
/// Fetches block editor settings from the WordPress REST API
81+
///
82+
/// - Returns: Raw settings data from the API
83+
@discardableResult
84+
private func fetchEditorSettings() async throws -> Data {
85+
let data = try await getData(for: baseURL.appendingPathComponent("/wp-block-editor/v1/settings"))
86+
do {
87+
createStoreDirectoryIfNeeded()
88+
try data.write(to: editorSettingsFileURL)
89+
} catch {
90+
assertionFailure("Failed to save settings: \(error)")
91+
}
92+
return data
93+
}
94+
95+
// MARK: - Private Helpers
96+
97+
private func createStoreDirectoryIfNeeded() {
98+
if !FileManager.default.fileExists(atPath: storeURL.path) {
99+
try? FileManager.default.createDirectory(at: storeURL, withIntermediateDirectories: true)
100+
}
101+
}
102+
103+
private func getData(for requestURL: URL) async throws -> Data {
104+
var request = URLRequest(url: requestURL)
105+
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
106+
107+
let (data, response) = try await urlSession.data(for: request)
108+
guard let status = (response as? HTTPURLResponse)?.statusCode,
109+
(200..<300).contains(status) else {
110+
throw URLError(.badServerResponse)
111+
}
112+
return data
113+
}
114+
}

0 commit comments

Comments
 (0)