-
Notifications
You must be signed in to change notification settings - Fork 3
Add support for Editor Settings #232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import Foundation | ||
|
|
||
| extension String { | ||
| /// Converts a string (such as a URL) into a safe directory name by removing illegal filesystem characters | ||
| /// | ||
| /// This method filters out characters that are not allowed in directory names across different filesystems, | ||
| /// including: `/`, `:`, `\`, `?`, `%`, `*`, `|`, `"`, `<`, `>`, newlines, and control characters. | ||
| /// | ||
| /// Example: | ||
| /// ```swift | ||
| /// let url = "https://example.com/path?query=1" | ||
| /// let safeName = url.safeFilename | ||
| /// // Result: "https---example.com-path-query-1" | ||
| /// ``` | ||
| var safeFilename: String { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could just replace it with sha256 as the folder name is only needed for technical purposes. wdyt?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, hashing is likely sufficient for this purpose. A readable name might make debugging easier, but that'd likely be infrequent and manageable.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was also leaning towards hashing. It's simpler and more robust. |
||
| // Define illegal characters for directory names | ||
| let illegalChars = CharacterSet(charactersIn: "/:\\?%*|\"<>") | ||
| .union(.newlines) | ||
| .union(.controlCharacters) | ||
|
|
||
| // Remove scheme and other URL components we don't want | ||
| var cleaned = self | ||
| if var urlComponents = URLComponents(string: self) { | ||
| urlComponents.scheme = nil | ||
| urlComponents.query = nil | ||
| urlComponents.fragment = nil | ||
| if let url = urlComponents.url?.absoluteString { | ||
| cleaned = url | ||
| } | ||
| } | ||
|
|
||
| // Trim and replace illegal characters with dashes | ||
| return cleaned | ||
| .trimmingCharacters(in: illegalChars) | ||
| .components(separatedBy: illegalChars) | ||
| .joined(separator: "-") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import Foundation | ||
|
|
||
| /// Service for fetching the editor settings and other parts of the environment | ||
| /// required to launch the editor. | ||
| public actor EditorService { | ||
| enum EditorServiceError: Error { | ||
| case invalidResponseData | ||
| } | ||
|
|
||
| private let siteID: String | ||
| private let baseURL: URL | ||
| private let authHeader: String | ||
| private let urlSession: URLSession | ||
|
|
||
| private let storeURL: URL | ||
| private var editorSettingsFileURL: URL { storeURL.appendingPathComponent("settings.json") } | ||
|
|
||
| private var refreshTask: Task<Void, Error>? | ||
|
|
||
| /// Creates a new EditorService instance | ||
| /// - Parameters: | ||
| /// - siteID: Unique identifier for the site (used for caching) | ||
| /// - baseURL: Root URL for the site API | ||
| /// - authHeader: Authorization header value | ||
| /// - urlSession: URLSession to use for network requests (defaults to .shared) | ||
| public init(siteID: String, baseURL: URL, authHeader: String, urlSession: URLSession = .shared) { | ||
| self.siteID = siteID | ||
| self.baseURL = baseURL | ||
| self.authHeader = authHeader | ||
| self.urlSession = urlSession | ||
|
|
||
| self.storeURL = URL.documentsDirectory | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should go into "Application" not "Documents". |
||
| .appendingPathComponent("GutenbergKit", isDirectory: true) | ||
| .appendingPathComponent(siteID.safeFilename, isDirectory: true) | ||
| } | ||
|
|
||
| /// Set up the editor for the given site. | ||
| /// | ||
| /// - warning: The request make take a significant amount of time the first | ||
| /// time you open the editor. | ||
| public func setup(_ configuration: inout EditorConfiguration) async throws { | ||
| var builder = configuration.toBuilder() | ||
|
|
||
| if !isEditorLoaded { | ||
| try await refresh() | ||
| } | ||
|
|
||
| if let data = try? Data(contentsOf: editorSettingsFileURL), | ||
| let settings = String(data: data, encoding: .utf8) { | ||
| builder = builder.setEditorSettings(settings) | ||
| } | ||
|
|
||
| return configuration = builder.build() | ||
| } | ||
|
|
||
| /// Returns `true` is the resources requied for the editor already exist. | ||
| private var isEditorLoaded: Bool { | ||
| FileManager.default.fileExists(atPath: editorSettingsFileURL.path()) | ||
| } | ||
|
|
||
| /// Refresh the editor resources. | ||
| public func refresh() async throws { | ||
| if let task = refreshTask { | ||
| return try await task.value | ||
| } | ||
| let task = Task { | ||
| defer { refreshTask = nil } | ||
| try await actuallyRefresh() | ||
| } | ||
| refreshTask = task | ||
| return try await task.value | ||
| } | ||
|
|
||
| private func actuallyRefresh() async throws { | ||
| try await fetchEditorSettings() | ||
| } | ||
|
|
||
| // MARK: – Editor Settings | ||
|
|
||
| /// Fetches block editor settings from the WordPress REST API | ||
| /// | ||
| /// - Returns: Raw settings data from the API | ||
| @discardableResult | ||
| private func fetchEditorSettings() async throws -> Data { | ||
| let data = try await getData(for: baseURL.appendingPathComponent("/wp-block-editor/v1/settings")) | ||
| do { | ||
| createStoreDirectoryIfNeeded() | ||
| try data.write(to: editorSettingsFileURL) | ||
| } catch { | ||
| assertionFailure("Failed to save settings: \(error)") | ||
| } | ||
| return data | ||
| } | ||
|
|
||
| // MARK: - Private Helpers | ||
|
|
||
| private func createStoreDirectoryIfNeeded() { | ||
| if !FileManager.default.fileExists(atPath: storeURL.path) { | ||
| try? FileManager.default.createDirectory(at: storeURL, withIntermediateDirectories: true) | ||
| } | ||
| } | ||
|
|
||
| private func getData(for requestURL: URL) async throws -> Data { | ||
| var request = URLRequest(url: requestURL) | ||
| request.setValue(authHeader, forHTTPHeaderField: "Authorization") | ||
|
|
||
| let (data, response) = try await urlSession.data(for: request) | ||
| guard let status = (response as? HTTPURLResponse)?.statusCode, | ||
| (200..<300).contains(status) else { | ||
| throw URLError(.badServerResponse) | ||
| } | ||
| return data | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.