diff --git a/CLAUDE.md b/CLAUDE.md index dbb073d7..fd960b22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,13 +98,10 @@ npm run clean:l10n # Clean only translations directory ```bash # Build Swift package -make build-swift-package - -# Build Swift package (force refresh of npm deps/translations if needed) -make build-swift-package REFRESH_DEPS=1 REFRESH_L10N=1 +swift build # Run Swift tests -make test-swift-package +swift test ``` ### Android Development diff --git a/Package.swift b/Package.swift index 151e29d2..9bb02fcb 100644 --- a/Package.swift +++ b/Package.swift @@ -24,11 +24,11 @@ let package = Package( .testTarget( name: "GutenbergKitTests", dependencies: ["GutenbergKit"], - path: "ios/Tests", + path: "ios/Tests/GutenbergKitTests", exclude: [], resources: [ - .process("GutenbergKitTests/Resources/") + .process("Resources") ] - ), + ) ] ) diff --git a/ios/Tests/GutenbergKitTests/Extensions/FoundationTests.swift b/ios/Tests/GutenbergKitTests/Extensions/FoundationTests.swift new file mode 100644 index 00000000..80748fca --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Extensions/FoundationTests.swift @@ -0,0 +1,323 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite +struct FileManagerExtensionsTests { + + // MARK: - fileExists(at:) + @Test("fileExists(at:) returns true for existing file") + func fileExistsReturnsTrueForExistingFile() throws { + let tempFile = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try Data("test".utf8).write(to: tempFile) + defer { try? FileManager.default.removeItem(at: tempFile) } + + #expect(FileManager.default.fileExists(at: tempFile) == true) + } + + @Test("fileExists(at:) returns false for non-existing file") + func fileExistsReturnsFalseForNonExistingFile() { + let nonExistentFile = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + #expect(FileManager.default.fileExists(at: nonExistentFile) == false) + } + + @Test("fileExists(at:) returns true for existing directory") + func fileExistsReturnsTrueForDirectory() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + #expect(FileManager.default.fileExists(at: tempDir) == true) + } + + // MARK: - directoryExists(at:) + @Test("directoryExists(at:) returns true for existing directory") + func directoryExistsReturnsTrueForDirectory() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + #expect(FileManager.default.directoryExists(at: tempDir) == true) + } + + @Test("directoryExists(at:) returns false for non-existing path") + func directoryExistsReturnsFalseForNonExisting() { + let nonExistentDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + #expect(FileManager.default.directoryExists(at: nonExistentDir) == false) + } + + @Test("directoryExists(at:) returns false for file") + func directoryExistsReturnsFalseForFile() throws { + let tempFile = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try Data("test".utf8).write(to: tempFile) + defer { try? FileManager.default.removeItem(at: tempFile) } + + #expect(FileManager.default.directoryExists(at: tempFile) == false) + } + + @Test("directoryExists(at:) distinguishes files from directories") + func directoryExistsDistinguishesFilesFromDirectories() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let tempFile = tempDir.appending(path: "file.txt") + try Data("test".utf8).write(to: tempFile) + + defer { try? FileManager.default.removeItem(at: tempDir) } + + #expect(FileManager.default.directoryExists(at: tempDir) == true) + #expect(FileManager.default.directoryExists(at: tempFile) == false) + #expect(FileManager.default.fileExists(at: tempFile) == true) + } + + // MARK: - Percent Encoding Tests + + @Test("fileExists(at:) handles paths with spaces") + func fileExistsHandlesPathsWithSpaces() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let fileWithSpaces = tempDir.appending(path: "file with spaces.txt") + try Data("test".utf8).write(to: fileWithSpaces) + + #expect(FileManager.default.fileExists(at: fileWithSpaces) == true) + } + + @Test("fileExists(at:) handles paths with percent-encoded characters") + func fileExistsHandlesPercentEncodedPaths() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + // Create a file with special characters that would be percent-encoded in URLs + let specialFileName = "file%20with%20encoded.txt" + let fileWithSpecialChars = tempDir.appending(path: specialFileName) + try Data("test".utf8).write(to: fileWithSpecialChars) + + #expect(FileManager.default.fileExists(at: fileWithSpecialChars) == true) + } + + @Test("fileExists(at:) handles paths with unicode characters") + func fileExistsHandlesUnicodePaths() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let unicodeFileName = "文件名.txt" + let fileWithUnicode = tempDir.appending(path: unicodeFileName) + try Data("test".utf8).write(to: fileWithUnicode) + + #expect(FileManager.default.fileExists(at: fileWithUnicode) == true) + } + + @Test("fileExists(at:) handles paths with special URL characters") + func fileExistsHandlesSpecialURLCharacters() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + // Characters that have special meaning in URLs: # ? & = + let specialFileName = "file#with?special&chars=test.txt" + let fileWithSpecialChars = tempDir.appending(path: specialFileName) + try Data("test".utf8).write(to: fileWithSpecialChars) + + #expect(FileManager.default.fileExists(at: fileWithSpecialChars) == true) + } + + @Test("directoryExists(at:) handles paths with spaces") + func directoryExistsHandlesPathsWithSpaces() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let dirWithSpaces = tempDir.appending(path: "directory with spaces") + try FileManager.default.createDirectory(at: dirWithSpaces, withIntermediateDirectories: true) + + #expect(FileManager.default.directoryExists(at: dirWithSpaces) == true) + } + + @Test("directoryExists(at:) handles paths with unicode characters") + func directoryExistsHandlesUnicodePaths() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let unicodeDirName = "目录名" + let dirWithUnicode = tempDir.appending(path: unicodeDirName) + try FileManager.default.createDirectory(at: dirWithUnicode, withIntermediateDirectories: true) + + #expect(FileManager.default.directoryExists(at: dirWithUnicode) == true) + } + + @Test("directoryExists(at:) handles nested paths with special characters") + func directoryExistsHandlesNestedSpecialPaths() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let nestedDir = + tempDir + .appending(path: "level 1 with spaces") + .appending(path: "level2#special") + .appending(path: "レベル3") + try FileManager.default.createDirectory(at: nestedDir, withIntermediateDirectories: true) + + #expect(FileManager.default.directoryExists(at: nestedDir) == true) + } + + @Test( + "fileExists(at:) correctly reports non-existence for percent-encoded path that doesn't exist") + func fileExistsReportsNonExistenceForEncodedPath() { + let nonExistentPath = FileManager.default.temporaryDirectory + .appending(path: UUID().uuidString) + .appending(path: "file with spaces.txt") + + #expect(FileManager.default.fileExists(at: nonExistentPath) == false) + } +} + +// MARK: - URL.appending(rawPath:) Tests + +@Suite +struct URLAppendingRawPathTests { + + // MARK: - Basic Path Appending + + @Test("appends path when base URL has no trailing slash and path has no leading slash") + func appendsPathWithNoSlashes() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + @Test("appends path when base URL has trailing slash and path has no leading slash") + func appendsPathWhenBaseHasTrailingSlash() { + let base = URL(string: "https://example.com/api/")! + let result = base.appending(rawPath: "posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + @Test("appends path when base URL has no trailing slash and path has leading slash") + func appendsPathWhenPathHasLeadingSlash() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "/posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + @Test("appends path when both have slashes (avoids double slash)") + func appendsPathAvoidingDoubleSlash() { + let base = URL(string: "https://example.com/api/")! + let result = base.appending(rawPath: "/posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + // MARK: - Multi-segment Paths + + @Test("appends multi-segment path") + func appendsMultiSegmentPath() { + let base = URL(string: "https://example.com/wp-json")! + let result = base.appending(rawPath: "wp/v2/posts") + #expect(result.absoluteString == "https://example.com/wp-json/wp/v2/posts") + } + + @Test("appends multi-segment path with leading slash") + func appendsMultiSegmentPathWithLeadingSlash() { + let base = URL(string: "https://example.com/wp-json")! + let result = base.appending(rawPath: "/wp/v2/posts") + #expect(result.absoluteString == "https://example.com/wp-json/wp/v2/posts") + } + + // MARK: - Query Parameters + + @Test("appends path with query parameters") + func appendsPathWithQueryParameters() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "posts?context=edit") + #expect(result.absoluteString == "https://example.com/api/posts?context=edit") + } + + @Test("appends path with multiple query parameters") + func appendsPathWithMultipleQueryParameters() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "posts?context=edit&per_page=10") + #expect(result.absoluteString == "https://example.com/api/posts?context=edit&per_page=10") + } + + @Test("preserves query parameters in base URL when appending path") + func preservesBaseQueryParameters() { + let base = URL(string: "https://example.com/api?token=abc")! + let result = base.appending(rawPath: "posts") + #expect(result.absoluteString == "https://example.com/api?token=abc/posts") + } + + // MARK: - Special Characters + + @Test("appends path with numeric ID") + func appendsPathWithNumericId() { + let base = URL(string: "https://example.com/api/posts")! + let result = base.appending(rawPath: "123") + #expect(result.absoluteString == "https://example.com/api/posts/123") + } + + @Test("appends path with hyphens and underscores") + func appendsPathWithHyphensAndUnderscores() { + let base = URL(string: "https://example.com")! + let result = base.appending(rawPath: "wp-block-editor/v1/settings_options") + #expect(result.absoluteString == "https://example.com/wp-block-editor/v1/settings_options") + } + + // MARK: - Edge Cases + + @Test("appends empty path") + func appendsEmptyPath() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "") + #expect(result.absoluteString == "https://example.com/api/") + } + + @Test("appends single slash") + func appendsSingleSlash() { + let base = URL(string: "https://example.com/api")! + let result = base.appending(rawPath: "/") + #expect(result.absoluteString == "https://example.com/api/") + } + + @Test("works with root URL") + func worksWithRootUrl() { + let base = URL(string: "https://example.com")! + let result = base.appending(rawPath: "api/posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + @Test("works with root URL with trailing slash") + func worksWithRootUrlWithTrailingSlash() { + let base = URL(string: "https://example.com/")! + let result = base.appending(rawPath: "api/posts") + #expect(result.absoluteString == "https://example.com/api/posts") + } + + // MARK: - Real-world WordPress API Paths + + @Test("appends WordPress post endpoint path") + func appendsWordPressPostEndpoint() { + let base = URL(string: "https://example.com/wp-json")! + let result = base.appending(rawPath: "wp/v2/posts/42?context=edit") + #expect(result.absoluteString == "https://example.com/wp-json/wp/v2/posts/42?context=edit") + } + + @Test("appends WordPress block editor settings path") + func appendsWordPressBlockEditorSettingsPath() { + let base = URL(string: "https://example.com/wp-json")! + let result = base.appending(rawPath: "wp-block-editor/v1/settings") + #expect(result.absoluteString == "https://example.com/wp-json/wp-block-editor/v1/settings") + } + + @Test("appends WordPress themes endpoint path") + func appendsWordPressThemesEndpoint() { + let base = URL(string: "https://example.com/wp-json")! + let result = base.appending(rawPath: "wp/v2/themes?status=active") + #expect(result.absoluteString == "https://example.com/wp-json/wp/v2/themes?status=active") + } +} diff --git a/ios/Tests/GutenbergKitTests/Model/EditorAssetBundleTests.swift b/ios/Tests/GutenbergKitTests/Model/EditorAssetBundleTests.swift new file mode 100644 index 00000000..c383baff --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Model/EditorAssetBundleTests.swift @@ -0,0 +1,576 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite +struct EditorAssetBundleTests { + + // MARK: - Initialization Tests + @Test("Default initialization creates bundle with empty manifest") + func defaultInitializationCreatesEmptyManifest() { + let bundle = makeBundle() + + #expect(bundle.manifest.scripts.isEmpty) + #expect(bundle.manifest.styles.isEmpty) + #expect(bundle.manifest.allowedBlockTypes.isEmpty) + } + + @Test("Default initialization sets downloadDate to current time") + func defaultInitializationSetsDownloadDate() { + let beforeCreation = Date() + let bundle = makeBundle() + let afterCreation = Date() + + #expect(bundle.downloadDate >= beforeCreation) + #expect(bundle.downloadDate <= afterCreation) + } + + @Test("Initialization with manifest preserves manifest data") + func initializationWithManifestPreservesData() throws { + let manifest = try createManifest( + scripts: "", + styles: "", + blockTypes: ["core/paragraph", "core/heading"] + ) + + let bundle = makeBundle(manifest: manifest) + + #expect(bundle.manifest.scripts.count == 1) + #expect(bundle.manifest.styles.count == 1) + #expect(bundle.manifest.allowedBlockTypes == ["core/paragraph", "core/heading"]) + } + + @Test("Initialization with custom downloadDate preserves date") + func initializationWithCustomDatePreservesDate() { + let customDate = Date(timeIntervalSince1970: 1_000_000) + let bundle = makeBundle(downloadDate: customDate) + + #expect(bundle.downloadDate == customDate) + } + + // MARK: - ID Tests + + @Test("Bundle ID equals manifest checksum") + func bundleIdEqualsManifestChecksum() throws { + let manifest = try createManifest(blockTypes: ["core/paragraph"]) + let bundle = makeBundle(manifest: manifest) + + #expect(bundle.id == manifest.checksum) + } + + @Test("Empty bundle has empty ID") + func emptyBundleHasIdEmpty() { + /// The bundle needs a non-nil ID so that it can be read back off the disk + let bundle = makeBundle() + #expect(bundle.id == "empty") + } + + @Test("Different manifests produce different bundle IDs") + func differentManifestsProduceDifferentIds() throws { + let manifest1 = try createManifest(blockTypes: ["core/paragraph"]) + let manifest2 = try createManifest(blockTypes: ["core/heading"]) + + let bundle1 = makeBundle(manifest: manifest1) + let bundle2 = makeBundle(manifest: manifest2) + + #expect(bundle1.id != bundle2.id) + } + + @Test("Same manifest data produces same bundle ID") + func sameManifestDataProducesSameId() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": ["core/paragraph"] + } + """ + + let manifest1 = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let manifest2 = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + + let bundle1 = makeBundle(manifest: manifest1) + let bundle2 = makeBundle(manifest: manifest2) + + #expect(bundle1.id == bundle2.id) + } + + // MARK: - assetCount Tests + + @Test("assetCount returns zero for empty bundle") + func assetCountReturnsZeroForEmptyBundle() { + let bundle = makeBundle() + #expect(bundle.assetCount == 0) + } + + @Test("assetCount reflects manifest asset URLs") + func assetCountReflectsManifestAssetUrls() throws { + let manifest = try createManifest( + scripts: "", + styles: "" + ) + let bundle = makeBundle(manifest: manifest) + + #expect(bundle.assetCount == 3) + } + + // MARK: - Codable Tests + + @Test("Bundle can be encoded and decoded") + func bundleCanBeEncodedAndDecoded() throws { + let manifest = try createManifest( + scripts: "", + blockTypes: ["core/paragraph", "core/image"] + ) + let originalBundle = makeBundle(manifest: manifest) + + let rawBundle = EditorAssetBundle.RawAssetBundle( + manifest: originalBundle.manifest, + downloadDate: originalBundle.downloadDate + ) + + let encoded = try JSONEncoder().encode(rawBundle) + let decoded = try JSONDecoder().decode(EditorAssetBundle.RawAssetBundle.self, from: encoded) + + #expect(decoded.manifest.checksum == originalBundle.manifest.checksum) + #expect(decoded.downloadDate == originalBundle.downloadDate) + #expect(decoded.manifest.allowedBlockTypes == originalBundle.manifest.allowedBlockTypes) + } + + @Test("Bundle preserves rawScripts through encoding") + func bundlePreservesRawScriptsThroughEncoding() throws { + let rawScripts = + "" + let manifest = try createManifest(scripts: rawScripts) + let originalBundle = makeBundle(manifest: manifest) + + let rawBundle = EditorAssetBundle.RawAssetBundle( + manifest: originalBundle.manifest, + downloadDate: originalBundle.downloadDate + ) + + let encoded = try JSONEncoder().encode(rawBundle) + let decoded = try JSONDecoder().decode(EditorAssetBundle.RawAssetBundle.self, from: encoded) + + #expect(decoded.manifest.rawScripts == originalBundle.manifest.rawScripts) + } + + @Test("Bundle preserves rawStyles through encoding") + func bundlePreservesRawStylesThroughEncoding() throws { + let rawStyles = + "" + let manifest = try createManifest(styles: rawStyles) + let originalBundle = makeBundle(manifest: manifest) + + let rawBundle = EditorAssetBundle.RawAssetBundle( + manifest: originalBundle.manifest, + downloadDate: originalBundle.downloadDate + ) + + let encoded = try JSONEncoder().encode(rawBundle) + let decoded = try JSONDecoder().decode(EditorAssetBundle.RawAssetBundle.self, from: encoded) + + #expect(decoded.manifest.rawStyles == originalBundle.manifest.rawStyles) + } + + // MARK: - URL Initialization Tests + + @Test("Bundle can be initialized from URL") + func bundleCanBeInitializedFromUrl() throws { + let manifest = try createManifest(blockTypes: ["core/paragraph"]) + let originalBundle = makeBundle(manifest: manifest) + + // Create temp directory structure + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + // Write manifest.json + let manifestURL = tempDir.appending(path: "manifest.json") + let rawBundle = EditorAssetBundle.RawAssetBundle( + manifest: originalBundle.manifest, + downloadDate: originalBundle.downloadDate + ) + let encoded = try JSONEncoder().encode(rawBundle) + try encoded.write(to: manifestURL) + + // Initialize from URL + let loadedBundle = try EditorAssetBundle(url: manifestURL) + + #expect(loadedBundle.id == originalBundle.id) + #expect(loadedBundle.manifest.allowedBlockTypes == originalBundle.manifest.allowedBlockTypes) + + // Clean up + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("Bundle initialization from invalid URL throws error") + func bundleInitializationFromInvalidUrlThrows() { + let invalidURL = URL(fileURLWithPath: "/nonexistent/path/bundle.json") + + #expect(throws: Error.self) { + _ = try EditorAssetBundle(url: invalidURL) + } + } + + @Test("Bundle initialization from invalid JSON throws error") + func bundleInitializationFromInvalidJsonThrows() throws { + let tempURL = FileManager.default.temporaryDirectory.appending( + path: "\(UUID().uuidString).json") + try Data("invalid json".utf8).write(to: tempURL) + + #expect(throws: Error.self) { + _ = try EditorAssetBundle(url: tempURL) + } + + // Clean up + try? FileManager.default.removeItem(at: tempURL) + } + + // MARK: - hasAssetData Tests + + @Test("hasAssetData returns false for non-existent file") + func hasAssetDataReturnsFalseForNonExistentFile() { + let bundle = makeBundle() + let url = URL(string: "https://example.com/nonexistent.js")! + + #expect(!bundle.hasAssetData(for: url)) + } + + @Test("hasAssetData returns true when file exists at expected path") + func hasAssetDataReturnsTrueWhenFileExists() throws { + // Create temp directory and file + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + let assetPath = tempDir.appending(path: "wp-content/plugins/script.js") + try FileManager.default.createDirectory( + at: assetPath.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + try Data("test".utf8).write(to: assetPath) + + let bundle = makeBundle(bundleRoot: tempDir) + let url = URL(string: "https://example.com/wp-content/plugins/script.js")! + + #expect(bundle.hasAssetData(for: url)) + + // Clean up + try? FileManager.default.removeItem(at: tempDir) + } + + // MARK: - assetDataPath Tests + + @Test("assetDataPath returns correct path based on URL path") + func assetDataPathReturnsCorrectPath() { + let tempDir = FileManager.default.temporaryDirectory.appending(path: "test-bundle") + let bundle = makeBundle(bundleRoot: tempDir) + + let url = URL(string: "https://example.com/wp-content/plugins/script.js")! + let result = bundle.assetDataPath(for: url) + + #expect(result.path.contains("/wp-content/plugins/script.js")) + } + + // MARK: - assetData Tests + + @Test("assetData returns data for existing file") + func assetDataReturnsDataForExistingFile() throws { + // Create temp directory and file + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + let assetPath = tempDir.appending(path: "script.js") + let testContent = "console.log('test');" + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + try Data(testContent.utf8).write(to: assetPath) + + let bundle = makeBundle(bundleRoot: tempDir) + + let requestUrl = URL(string: "https://example.com/script.js")! + let data = try bundle.assetData(for: requestUrl) + + #expect(String(data: data, encoding: .utf8) == testContent) + + // Clean up + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("assetData throws when file doesn't exist") + func assetDataThrowsWhenFileDoesntExist() { + let bundle = makeBundle() + let url = URL(string: "https://example.com/nonexistent.js")! + + #expect(throws: Error.self) { + _ = try bundle.assetData(for: url) + } + } + + // MARK: - Equatable Tests + + @Test("Equal bundles are equal") + func equalBundlesAreEqual() throws { + let manifest = try createManifest(blockTypes: ["core/paragraph"]) + let date = Date(timeIntervalSince1970: 1_700_000_000) + + let bundle1 = makeBundle(manifest: manifest, downloadDate: date) + let bundle2 = makeBundle(manifest: manifest, downloadDate: date) + + #expect(bundle1 == bundle2) + } + + @Test("Bundles with different manifests are not equal") + func bundlesWithDifferentManifestsNotEqual() throws { + let manifest1 = try createManifest(blockTypes: ["core/paragraph"]) + let manifest2 = try createManifest(blockTypes: ["core/heading"]) + + let bundle1 = makeBundle(manifest: manifest1) + let bundle2 = makeBundle(manifest: manifest2) + + #expect(bundle1 != bundle2) + } + + @Test("Bundles with different downloadDates are not equal") + func bundlesWithDifferentDatesNotEqual() throws { + let manifest = try createManifest(blockTypes: ["core/paragraph"]) + + let bundle1 = makeBundle(manifest: manifest, downloadDate: Date(timeIntervalSince1970: 1000)) + let bundle2 = makeBundle(manifest: manifest, downloadDate: Date(timeIntervalSince1970: 2000)) + + #expect(bundle1 != bundle2) + } + + // MARK: - Integration Tests + + @Test("Bundle round-trip through file system preserves all data") + func bundleRoundTripPreservesAllData() throws { + let manifest = try createManifest( + scripts: "", + styles: "", + blockTypes: ["core/paragraph", "core/heading", "jetpack/ai-assistant"] + ) + let customDate = Date(timeIntervalSince1970: 1_700_000_000) + let originalBundle = makeBundle(manifest: manifest, downloadDate: customDate) + + // Create temp directory + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + // Write to file + let tempURL = tempDir.appending(path: "manifest.json") + let rawBundle = EditorAssetBundle.RawAssetBundle( + manifest: originalBundle.manifest, + downloadDate: originalBundle.downloadDate + ) + let encoded = try JSONEncoder().encode(rawBundle) + try encoded.write(to: tempURL) + + // Read back + let loadedBundle = try EditorAssetBundle(url: tempURL) + + // Verify all data preserved + #expect(loadedBundle.id == originalBundle.id) + #expect(loadedBundle.downloadDate == originalBundle.downloadDate) + #expect(loadedBundle.manifest.scripts == originalBundle.manifest.scripts) + #expect(loadedBundle.manifest.styles == originalBundle.manifest.styles) + #expect(loadedBundle.manifest.allowedBlockTypes == originalBundle.manifest.allowedBlockTypes) + #expect(loadedBundle.manifest.rawScripts == originalBundle.manifest.rawScripts) + #expect(loadedBundle.manifest.rawStyles == originalBundle.manifest.rawStyles) + #expect(loadedBundle.manifest.checksum == originalBundle.manifest.checksum) + + // Clean up + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("Multiple bundles with different dates have same ID if same manifest") + func multipleBundlesWithDifferentDatesHaveSameId() throws { + let manifest = try createManifest(blockTypes: ["core/paragraph"]) + + let bundle1 = makeBundle( + manifest: manifest, + downloadDate: Date(timeIntervalSince1970: 1000) + ) + + let bundle2 = makeBundle( + manifest: manifest, + downloadDate: Date(timeIntervalSince1970: 2000) + ) + + #expect(bundle1.id == bundle2.id) + #expect(bundle1.downloadDate != bundle2.downloadDate) + } + + // MARK: - EditorRepresentation Tests + + @Test("setEditorRepresentation writes file to bundle root") + func setEditorRepresentationWritesFile() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let bundle = makeBundle(bundleRoot: tempDir) + let representation = RemoteEditorAssetManifest.RawManifest( + scripts: "", + styles: "", + allowedBlockTypes: ["core/paragraph"] + ) + + try bundle.setEditorRepresentation(representation) + + let filePath = tempDir.appending(path: "editor-representation.json") + #expect(FileManager.default.fileExists(atPath: filePath.path)) + + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("getEditorRepresentation returns typed EditorRepresentation") + func getEditorRepresentationReturnsTypedValue() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let bundle = makeBundle(bundleRoot: tempDir) + let original = RemoteEditorAssetManifest.RawManifest( + scripts: "", + styles: "", + allowedBlockTypes: ["core/paragraph", "core/heading"] + ) + + try bundle.setEditorRepresentation(original) + + let retrieved: EditorAssetBundle.EditorRepresentation = try bundle.getEditorRepresentation() + + #expect(retrieved.scripts == original.scripts) + #expect(retrieved.styles == original.styles) + #expect(retrieved.allowedBlockTypes == original.allowedBlockTypes) + + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("getEditorRepresentation returns Any for JSON serialization") + func getEditorRepresentationReturnsAny() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let bundle = makeBundle(bundleRoot: tempDir) + let original = RemoteEditorAssetManifest.RawManifest( + scripts: "", + styles: "", + allowedBlockTypes: ["core/image"] + ) + + try bundle.setEditorRepresentation(original) + + let retrieved: Any = try bundle.getEditorRepresentation() + + #expect(retrieved is [String: Any]) + let dict = retrieved as! [String: Any] + #expect(dict["scripts"] as? String == original.scripts) + #expect(dict["styles"] as? String == original.styles) + #expect(dict["allowed_block_types"] as? [String] == original.allowedBlockTypes) + + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("getEditorRepresentation throws when file does not exist") + func getEditorRepresentationThrowsWhenFileDoesNotExist() { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + let bundle = makeBundle(bundleRoot: tempDir) + + #expect(throws: Error.self) { + let _: EditorAssetBundle.EditorRepresentation = try bundle.getEditorRepresentation() + } + } + + @Test("setEditorRepresentation overwrites existing file") + func setEditorRepresentationOverwritesExistingFile() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let bundle = makeBundle(bundleRoot: tempDir) + + let first = RemoteEditorAssetManifest.RawManifest( + scripts: "first", + styles: "first", + allowedBlockTypes: ["first"] + ) + try bundle.setEditorRepresentation(first) + + let second = RemoteEditorAssetManifest.RawManifest( + scripts: "second", + styles: "second", + allowedBlockTypes: ["second"] + ) + try bundle.setEditorRepresentation(second) + + let retrieved: EditorAssetBundle.EditorRepresentation = try bundle.getEditorRepresentation() + + #expect(retrieved.scripts == "second") + #expect(retrieved.styles == "second") + #expect(retrieved.allowedBlockTypes == ["second"]) + + try? FileManager.default.removeItem(at: tempDir) + } + + @Test("EditorRepresentation round-trip preserves all fields") + func editorRepresentationRoundTripPreservesAllFields() throws { + let tempDir = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + + let bundle = makeBundle(bundleRoot: tempDir) + let original = RemoteEditorAssetManifest.RawManifest( + scripts: "", + styles: "", + allowedBlockTypes: ["core/paragraph", "core/heading", "core/image", "jetpack/ai-assistant"] + ) + + try bundle.setEditorRepresentation(original) + let retrieved: EditorAssetBundle.EditorRepresentation = try bundle.getEditorRepresentation() + + #expect(retrieved == original) + + try? FileManager.default.removeItem(at: tempDir) + } +} + +// MARK: - Test Helpers + +extension EditorAssetBundleTests { + + fileprivate func makeBundle( + manifest: LocalEditorAssetManifest = .empty, + downloadDate: Date? = nil, + bundleRoot: URL = .temporaryDirectory + ) -> EditorAssetBundle { + if let downloadDate { + return try! EditorAssetBundle(manifest: manifest, downloadDate: downloadDate, bundleRoot: bundleRoot) + } + return try! EditorAssetBundle(manifest: manifest, bundleRoot: bundleRoot) + } + + fileprivate func createManifest( + scripts: String = "", + styles: String = "", + blockTypes: [String] = [] + ) throws -> LocalEditorAssetManifest { + let blockTypesJson = blockTypes.map { "\"\($0)\"" }.joined(separator: ", ") + let json = """ + { + "scripts": \(escapeJsonString(scripts)), + "styles": \(escapeJsonString(styles)), + "allowed_block_types": [\(blockTypesJson)] + } + """ + return try LocalEditorAssetManifest.from(data: Data(json.utf8)) + } + + fileprivate func escapeJsonString(_ string: String) -> String { + let escaped = + string + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + .replacingOccurrences(of: "\n", with: "\\n") + return "\"\(escaped)\"" + } +} + +extension LocalEditorAssetManifest { + fileprivate static func from(data: Data) throws -> LocalEditorAssetManifest { + let remote = try RemoteEditorAssetManifest(data: data) + return try LocalEditorAssetManifest(remoteManifest: remote) + } +} diff --git a/ios/Tests/GutenbergKitTests/Model/EditorCachePolicyTests.swift b/ios/Tests/GutenbergKitTests/Model/EditorCachePolicyTests.swift new file mode 100644 index 00000000..7daf80c4 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Model/EditorCachePolicyTests.swift @@ -0,0 +1,119 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite +struct EditorCachePolicyTests { + + // MARK: - Test Fixtures + + /// A fixed reference date for deterministic testing. + static let referenceDate = Date(timeIntervalSinceReferenceDate: 0) + + // MARK: - ignore Policy Tests + + @Test("ignore policy always returns false") + func ignorePolicyAlwaysReturnsFalse() { + let policy = EditorCachePolicy.ignore + + // Even a response cached just now should not be allowed + #expect(policy.allowsResponseWith(date: Self.referenceDate, currentDate: Self.referenceDate) == false) + } + + // MARK: - always Policy Tests + @Test("always policy always returns true") + func alwaysPolicyAlwaysReturnsTrue() { + let policy = EditorCachePolicy.always + + // Even an extremely old response should be allowed + #expect(policy.allowsResponseWith(date: Self.referenceDate, currentDate: Self.referenceDate) == true) + } + + // MARK: - maxAge Policy Tests + @Test("maxAge policy returns true for response cached just now") + func maxAgePolicyReturnsTrueForJustCached() { + let policy = EditorCachePolicy.maxAge(60) // 60 seconds + + #expect(policy.allowsResponseWith(date: Self.referenceDate, currentDate: Self.referenceDate) == true) + } + + @Test("maxAge policy returns true for fresh response within interval") + func maxAgePolicyReturnsTrueForFreshResponse() { + let policy = EditorCachePolicy.maxAge(60) // 60 seconds + let thirtySecondsAgo = Self.referenceDate.addingTimeInterval(-30) + + #expect(policy.allowsResponseWith(date: thirtySecondsAgo, currentDate: Self.referenceDate) == true) + } + + @Test("maxAge policy returns false for expired response") + func maxAgePolicyReturnsFalseForExpiredResponse() { + let policy = EditorCachePolicy.maxAge(60) // 60 seconds + let twoMinutesAgo = Self.referenceDate.addingTimeInterval(-120) + + #expect(policy.allowsResponseWith(date: twoMinutesAgo, currentDate: Self.referenceDate) == false) + } + + @Test("maxAge policy returns false for response just past expiry") + func maxAgePolicyReturnsFalseForJustPastExpiry() { + let policy = EditorCachePolicy.maxAge(60) // 60 seconds + let sixtyOneSecondsAgo = Self.referenceDate.addingTimeInterval(-61) + + #expect(policy.allowsResponseWith(date: sixtyOneSecondsAgo, currentDate: Self.referenceDate) == false) + } + + @Test("maxAge policy returns true for future-dated response") + func maxAgePolicyReturnsTrueForFutureResponse() { + let policy = EditorCachePolicy.maxAge(60) // 60 seconds + let tenMinutesFromNow = Self.referenceDate.addingTimeInterval(600) + + // A future-dated response is definitely not expired + #expect(policy.allowsResponseWith(date: tenMinutesFromNow, currentDate: Self.referenceDate) == true) + } + + // MARK: - maxAge Policy with Different Intervals + + @Test("maxAge policy works with zero interval") + func maxAgePolicyWorksWithZeroInterval() { + let policy = EditorCachePolicy.maxAge(0) + + // With zero interval, only future-dated responses are valid + #expect(policy.allowsResponseWith(date: Self.referenceDate, currentDate: Self.referenceDate) == false) + #expect( + policy.allowsResponseWith( + date: Self.referenceDate.addingTimeInterval(-1), currentDate: Self.referenceDate) == false) + } + + @Test("maxAge policy works with one hour interval") + func maxAgePolicyWorksWithOneHourInterval() { + let policy = EditorCachePolicy.maxAge(3600) // 1 hour + + let thirtyMinutesAgo = Self.referenceDate.addingTimeInterval(-1800) + let twoHoursAgo = Self.referenceDate.addingTimeInterval(-7200) + + #expect(policy.allowsResponseWith(date: thirtyMinutesAgo, currentDate: Self.referenceDate) == true) + #expect(policy.allowsResponseWith(date: twoHoursAgo, currentDate: Self.referenceDate) == false) + } + + @Test("maxAge policy works with one day interval") + func maxAgePolicyWorksWithOneDayInterval() { + let policy = EditorCachePolicy.maxAge(86400) // 24 hours + + let twelveHoursAgo = Self.referenceDate.addingTimeInterval(-43200) + let twoDaysAgo = Self.referenceDate.addingTimeInterval(-172800) + + #expect(policy.allowsResponseWith(date: twelveHoursAgo, currentDate: Self.referenceDate) == true) + #expect(policy.allowsResponseWith(date: twoDaysAgo, currentDate: Self.referenceDate) == false) + } + + @Test("maxAge policy works with very large interval") + func maxAgePolicyWorksWithVeryLargeInterval() { + let policy = EditorCachePolicy.maxAge(365 * 24 * 60 * 60) // 1 year + + let sixMonthsAgo = Self.referenceDate.addingTimeInterval(-182 * 24 * 60 * 60) + let twoYearsAgo = Self.referenceDate.addingTimeInterval(-730 * 24 * 60 * 60) + + #expect(policy.allowsResponseWith(date: sixMonthsAgo, currentDate: Self.referenceDate) == true) + #expect(policy.allowsResponseWith(date: twoYearsAgo, currentDate: Self.referenceDate) == false) + } +} diff --git a/ios/Tests/GutenbergKitTests/Model/EditorConfigurationTests.swift b/ios/Tests/GutenbergKitTests/Model/EditorConfigurationTests.swift new file mode 100644 index 00000000..dfa22664 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Model/EditorConfigurationTests.swift @@ -0,0 +1,545 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite +struct EditorConfigurationBuilderTests: MakesTestFixtures { + // MARK: - Test Fixtures + + static let testSiteURL = URL(string: "https://example.com")! + static let testApiRoot = URL(string: "https://example.com/wp-json")! + + // MARK: - Default Values Tests + + @Test("Builder uses correct default values") + func builderDefaultValues() { + let config = makeConfigurationBuilder().build() + + #expect(config.title == "") + #expect(config.content == "") + #expect(config.postID == nil) + #expect(config.postType == "post") + #expect(config.shouldUseThemeStyles == false) + #expect(config.shouldUsePlugins == false) + #expect(config.shouldHideTitle == false) + #expect(config.siteURL == Self.testSiteURL) + #expect(config.siteApiRoot == Self.testApiRoot) + #expect(config.siteApiNamespace == []) + #expect(config.namespaceExcludedPaths == []) + #expect(config.authHeader == "") + #expect(config.editorSettings == "undefined") + #expect(config.locale == "en") + #expect(config.isNativeInserterEnabled == false) + #expect(config.logLevel == .error) + #expect(config.enableNetworkLogging == false) + } + + // MARK: - Individual Setter Tests + + @Test("setTitle updates title") + func setTitleUpdatesTitle() { + let config = makeConfigurationBuilder() + .setTitle("My Post Title") + .build() + + #expect(config.title == "My Post Title") + } + + @Test("setContent updates content") + func setContentUpdatesContent() { + let config = makeConfigurationBuilder() + .setContent("
Hello world
") + .build() + + #expect(config.content == "Hello world
") + } + + @Test("setPostID updates postID") + func setPostIDUpdatesPostID() { + let config = makeConfigurationBuilder() + .setPostID(123) + .build() + + #expect(config.postID == 123) + } + + @Test("setPostID with nil clears postID") + func setPostIDWithNilClearsPostID() { + let config = makeConfigurationBuilder() + .setPostID(123) + .setPostID(nil) + .build() + + #expect(config.postID == nil) + } + + @Test("setPostType updates postType") + func setPostTypeUpdatesPostType() { + let config = makeConfigurationBuilder() + .setPostType("page") + .build() + + #expect(config.postType == "page") + } + + @Test("setShouldUseThemeStyles updates shouldUseThemeStyles") + func setShouldUseThemeStylesUpdates() { + let config = makeConfigurationBuilder() + .setShouldUseThemeStyles(true) + .build() + + #expect(config.shouldUseThemeStyles == true) + } + + @Test("setShouldUsePlugins updates shouldUsePlugins") + func setShouldUsePluginsUpdates() { + let config = makeConfigurationBuilder() + .setShouldUsePlugins(true) + .build() + + #expect(config.shouldUsePlugins == true) + } + + @Test("setShouldHideTitle updates shouldHideTitle") + func setShouldHideTitleUpdates() { + let config = makeConfigurationBuilder() + .setShouldHideTitle(true) + .build() + + #expect(config.shouldHideTitle == true) + } + + @Test("setSiteUrl updates siteURL") + func setSiteUrlUpdatesSiteURL() { + let newURL = URL(string: "https://other.com")! + let config = makeConfigurationBuilder() + .setSiteUrl(newURL) + .build() + + #expect(config.siteURL == newURL) + } + + @Test("setSiteApiRoot updates siteApiRoot") + func setSiteApiRootUpdatesSiteApiRoot() { + let newURL = URL(string: "https://other.com/wp-json/v2")! + let config = makeConfigurationBuilder() + .setSiteApiRoot(newURL) + .build() + + #expect(config.siteApiRoot == newURL) + } + + @Test("setSiteApiNamespace updates siteApiNamespace") + func setSiteApiNamespaceUpdates() { + let namespaces = ["wp/v2", "wp/v3"] + let config = makeConfigurationBuilder() + .setSiteApiNamespace(namespaces) + .build() + + #expect(config.siteApiNamespace == namespaces) + } + + @Test("setNamespaceExcludedPaths updates namespaceExcludedPaths") + func setNamespaceExcludedPathsUpdates() { + let paths = ["/oembed", "/batch"] + let config = makeConfigurationBuilder() + .setNamespaceExcludedPaths(paths) + .build() + + #expect(config.namespaceExcludedPaths == paths) + } + + @Test("setAuthHeader updates authHeader") + func setAuthHeaderUpdatesAuthHeader() { + let config = makeConfigurationBuilder() + .setAuthHeader("Bearer token123") + .build() + + #expect(config.authHeader == "Bearer token123") + } + + @Test("setEditorSettings updates editorSettings") + func setEditorSettingsUpdatesEditorSettings() { + let settings = #"{"colors":[]}"# + let config = makeConfigurationBuilder() + .setEditorSettings(settings) + .build() + + #expect(config.editorSettings == settings) + } + + @Test("setLocale updates locale") + func setLocaleUpdatesLocale() { + let config = makeConfigurationBuilder() + .setLocale("fr_FR") + .build() + + #expect(config.locale == "fr_FR") + } + + @Test("setNativeInserterEnabled updates isNativeInserterEnabled") + func setNativeInserterEnabledUpdates() { + let config = makeConfigurationBuilder() + .setNativeInserterEnabled(true) + .build() + + #expect(config.isNativeInserterEnabled == true) + } + + @Test("setNativeInserterEnabled defaults to true") + func setNativeInserterEnabledDefaultsToTrue() { + let config = makeConfigurationBuilder() + .setNativeInserterEnabled() + .build() + + #expect(config.isNativeInserterEnabled == true) + } + + @Test("setLogLevel updates logLevel") + func setLogLevelUpdatesLogLevel() { + let config = makeConfigurationBuilder() + .setLogLevel(.debug) + .build() + + #expect(config.logLevel == .debug) + } + + @Test("setEnableNetworkLogging updates enableNetworkLogging") + func setEnableNetworkLoggingUpdates() { + let config = makeConfigurationBuilder() + .setEnableNetworkLogging(true) + .build() + + #expect(config.enableNetworkLogging == true) + } + + // MARK: - Method Chaining Tests + + @Test("Builder supports method chaining") + func builderMethodChaining() { + let config = makeConfigurationBuilder() + .setTitle("Chained Title") + .setContent("Chained content
") + .setPostID(456) + .setShouldUsePlugins(true) + .setShouldUseThemeStyles(true) + .setLocale("de_DE") + .setLogLevel(.info) + .build() + + #expect(config.title == "Chained Title") + #expect(config.content == "Chained content
") + #expect(config.postID == 456) + #expect(config.shouldUsePlugins == true) + #expect(config.shouldUseThemeStyles == true) + #expect(config.locale == "de_DE") + #expect(config.logLevel == .info) + } + + // MARK: - Builder Immutability Tests + @Test("Builder setters return new instance without modifying original") + func builderSettersReturnNewInstance() { + let builder1 = makeConfigurationBuilder() + let builder2 = builder1.setTitle("New Title") + + let config1 = builder1.build() + let config2 = builder2.build() + + #expect(config1.title == "") + #expect(config2.title == "New Title") + } + + @Test("Multiple builds from same builder produce equal configs") + func multipleBuildsSameBuilder() { + let builder = makeConfigurationBuilder().setTitle("Test") + + let config1 = builder.build() + let config2 = builder.build() + + #expect(config1 == config2) + } + + // MARK: - apply() Conditional Method Tests + @Test("apply with non-nil value applies closure") + func applyWithNonNilValue() { + let postID: Int? = 123 + let config = makeConfigurationBuilder() + .apply(postID) { builder, value in builder.setPostID(value) } + .build() + + #expect(config.postID == 123) + } + + @Test("apply with nil value skips closure") + func applyWithNilValue() { + let postID: Int? = nil + let config = makeConfigurationBuilder() + .apply(postID) { + #expect(Bool(false), "This callback should never be invoked") + return $0.setPostID($1) + } + .build() + + #expect(config.postID == nil) + } + + @Test("apply can be chained multiple times") + func applyChainedMultipleTimes() { + let postID: Int? = 42 + let title: String? = "Applied Title" + let content: String? = nil + + let config = makeConfigurationBuilder() + .apply(postID) { $0.setPostID($1) } + .apply(title) { $0.setTitle($1) } + .apply(content) { $0.setContent($1) } + .build() + + #expect(config.postID == 42) + #expect(config.title == "Applied Title") + #expect(config.content == "") + } + + // MARK: - toBuilder Round-Trip Tests + @Test("toBuilder preserves all configuration values") + func toBuilderRoundTrip() { + let original = makeConfigurationBuilder() + .setTitle("Round Trip Title") + .setContent("Round trip content
") + .setPostID(789) + .setShouldUseThemeStyles(true) + .setShouldUsePlugins(true) + .setShouldHideTitle(true) + .setSiteApiNamespace(["wp/v2"]) + .setNamespaceExcludedPaths(["/oembed"]) + .setAuthHeader("Bearer abc") + .setEditorSettings("{}") + .setLocale("ja_JP") + .setNativeInserterEnabled(true) + .setLogLevel(.debug) + .setEnableNetworkLogging(true) + .build() + + #expect(original.toBuilder().build() == original) + } + + @Test("toBuilder allows modification of existing config") + func toBuilderAllowsModification() { + let original = makeConfigurationBuilder() + .setTitle("Original Title") + .setPostID(100) + .build() + + let modified = original.toBuilder() + .setTitle("Modified Title") + .build() + + #expect(original.title == "Original Title") + #expect(modified.title == "Modified Title") + #expect(modified.postID == 100) + } +} + +@Suite +struct EditorConfigurationTests: MakesTestFixtures { + + // MARK: - Test Fixtures + static let testSiteURL = URL(string: "https://example.com")! + static let testApiRoot = URL(string: "https://example.com/wp-json")! + + // MARK: - siteId Derivation Tests + @Test("siteId extracts host from siteURL") + func siteIdExtractsHost() { + #expect( + makeConfiguration(siteURL: URL(string: "https://example.com/path/to/site")!).siteId + == "example.com") + } + + @Test("siteId handles subdomain") + func siteIdHandlesSubdomain() { + #expect( + makeConfiguration(siteURL: URL(string: "https://blog.example.com")!).siteId + == "blog.example.com") + } + + @Test("siteId handles deep subdomain") + func siteIdHandlesDeepSubdomain() { + #expect( + makeConfiguration(siteURL: URL(string: "https://dev.blog.example.com")!).siteId + == "dev.blog.example.com") + } + + @Test("siteId handles internationalized domain") + func siteIdHandlesInternationalizedDomain() { + // URL converts IDN to punycode + #expect(makeConfiguration(siteURL: URL(string: "https://例え.jp")!).siteId == "xn--r8jz45g.jp") + } + + @Test("siteId handles localhost") + func siteIdHandlesLocalhost() { + #expect( + makeConfiguration(siteURL: URL(string: "http://localhost:8080/wordpress")!).siteId + == "localhost") + } + + @Test("siteId handles IP address") + func siteIdHandlesIPAddress() { + #expect( + makeConfiguration(siteURL: URL(string: "http://192.168.1.1/wordpress")!).siteId + == "192.168.1.1") + } + + // MARK: - escapedTitle Tests + + @Test("escapedTitle percent-encodes spaces") + func escapedTitleEncodesSpaces() { + #expect(makeConfiguration(title: "Hello World").escapedTitle == "Hello%20World") + } + + @Test("escapedTitle percent-encodes special characters") + func escapedTitleEncodesSpecialChars() { + #expect( + makeConfiguration(title: "Title with & and ?").escapedTitle + == "Title%20with%20%26%20and%20%3F") + } + + @Test("escapedTitle percent-encodes unicode") + func escapedTitleEncodesUnicode() { + #expect( + makeConfiguration(title: "こんにちは").escapedTitle + == "%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF") + } + + @Test("escapedTitle handles empty string") + func escapedTitleHandlesEmpty() { + #expect(makeConfiguration(title: "").escapedTitle == "") + } + + @Test("escapedTitle preserves alphanumeric characters") + func escapedTitlePreservesAlphanumeric() { + #expect(makeConfiguration(title: "Title123").escapedTitle == "Title123") + } + + // MARK: - escapedContent Tests + @Test("escapedContent percent-encodes HTML") + func escapedContentEncodesHTML() { + #expect(makeConfiguration(content: "Test
").escapedContent == "%3Cp%3ETest%3C%2Fp%3E") + } + + @Test("escapedContent percent-encodes complex HTML") + func escapedContentEncodesComplexHTML() { + #expect( + makeConfiguration(content: #"Content
") + let config2 = makeConfiguration(title: "Test", content: "Content
") + + #expect(config1 == config2) + } + + @Test("Configurations with different title are not equal") + func differentTitleNotEqual() { + let config1 = makeConfiguration(title: "Title 1") + let config2 = makeConfiguration(title: "Title 2") + + #expect(config1 != config2) + } + + @Test("Configurations with different content are not equal") + func differentContentNotEqual() { + let config1 = makeConfiguration(content: "Content 1") + let config2 = makeConfiguration(content: "Content 2") + + #expect(config1 != config2) + } + + @Test("Configurations with different postID are not equal") + func differentPostIDNotEqual() { + let config1 = EditorConfigurationBuilder( + postType: "post", + siteURL: Self.testSiteURL, + siteApiRoot: Self.testApiRoot + ).setPostID(1).build() + + let config2 = EditorConfigurationBuilder( + postType: "post", + siteURL: Self.testSiteURL, + siteApiRoot: Self.testApiRoot + ).setPostID(2).build() + + #expect(config1 != config2) + } + + @Test("Configurations with different siteURL are not equal") + func differentSiteURLNotEqual() { + let config1 = makeConfiguration(siteURL: URL(string: "https://site1.com")!) + let config2 = makeConfiguration(siteURL: URL(string: "https://site2.com")!) + + #expect(config1 != config2) + } + + // MARK: - Hashable Tests + + @Test("Identical configurations have same hash") + func identicalConfigsHaveSameHash() { + let config1 = makeConfiguration(title: "Test", content: "Content") + let config2 = makeConfiguration(title: "Test", content: "Content") + + #expect(config1.hashValue == config2.hashValue) + } + + @Test("Configurations can be used in Set") + func configurationsCanBeUsedInSet() { + let config1 = EditorConfigurationBuilder( + postType: "post", + siteURL: Self.testSiteURL, + siteApiRoot: Self.testApiRoot + ).setPostID(1).build() + + let config2 = EditorConfigurationBuilder( + postType: "post", + siteURL: Self.testSiteURL, + siteApiRoot: Self.testApiRoot + ).setPostID(2).build() + + let config3 = EditorConfigurationBuilder( + postType: "post", + siteURL: Self.testSiteURL, + siteApiRoot: Self.testApiRoot + ).setPostID(1).build() + + let set: SetTest
") + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + #expect(global.post.content.contains("%")) + #expect(!global.post.content.contains("<")) + } + + // MARK: - toString() + + @Test("toString produces valid JSON") + func toStringProducesValidJson() throws { + let configuration = makeConfiguration() + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try global.toString() + let data = Data(jsonString.utf8) + let decoded = try JSONSerialization.jsonObject(with: data) + + #expect(decoded is [String: Any]) + } + + @Test("toString includes all required fields") + func toStringIncludesAllFields() throws { + let configuration = makeConfiguration(postID: 123, title: "Test", content: "Content") + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try global.toString() + + #expect(jsonString.contains("siteURL")) + #expect(jsonString.contains("siteApiRoot")) + #expect(jsonString.contains("themeStyles")) + #expect(jsonString.contains("plugins")) + #expect(jsonString.contains("post")) + #expect(jsonString.contains("locale")) + #expect(jsonString.contains("logLevel")) + } + + @Test("toString round-trips through Codable") + func toStringRoundTrips() throws { + let configuration = makeConfiguration(postID: 99, title: "Round Trip", content: "Test content") + let original = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try original.toString() + let data = Data(jsonString.utf8) + let decoded = try JSONDecoder().decode(GBKitGlobal.self, from: data) + + #expect(decoded.siteURL == original.siteURL) + #expect(decoded.post.id == original.post.id) + #expect(decoded.post.title == original.post.title) + #expect(decoded.themeStyles == original.themeStyles) + #expect(decoded.plugins == original.plugins) + } + + // MARK: - Special Characters + + @Test("handles unicode in title") + func handlesUnicodeInTitle() throws { + let configuration = makeConfiguration(title: "日本語タイトル") + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try global.toString() + #expect(!jsonString.isEmpty) + + let data = Data(jsonString.utf8) + let decoded = try JSONDecoder().decode(GBKitGlobal.self, from: data) + #expect(decoded.post.title == global.post.title) + } + + @Test("handles emoji in content") + func handlesEmojiInContent() throws { + let configuration = makeConfiguration(content: "Hello 👋 World 🌍") + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try global.toString() + #expect(!jsonString.isEmpty) + + let data = Data(jsonString.utf8) + let decoded = try JSONDecoder().decode(GBKitGlobal.self, from: data) + #expect(decoded.post.content == global.post.content) + } + + @Test("handles special HTML characters in content") + func handlesHtmlCharactersInContent() throws { + let configuration = makeConfiguration(content: "") + let global = try GBKitGlobal(configuration: configuration, dependencies: makeDependencies()) + + let jsonString = try global.toString() + // Should be percent-encoded, not raw HTML + #expect(!jsonString.contains("", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.contains(URL(string: "https://example.com/app.js")!)) + #expect(links.contains(URL(string: "https://example.com/vendor.js")!)) + } + + @Test("parses stylesheet href attributes") + func parsesStylesheetHref() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.contains(URL(string: "https://example.com/main.css")!)) + #expect(links.contains(URL(string: "https://example.com/theme.css")!)) + } + + @Test("parses both scripts and styles") + func parsesBothScriptsAndStyles() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.count == 2) + #expect(links.contains(URL(string: "https://example.com/app.js")!)) + #expect(links.contains(URL(string: "https://example.com/style.css")!)) + } + + @Test("resolves protocol-relative URLs with default scheme") + func resolvesProtocolRelativeURLs() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + + let linksWithHttps = manifest.assetUrls + #expect(linksWithHttps.contains(URL(string: "https://cdn.example.com/script.js")!)) + #expect(linksWithHttps.contains(URL(string: "https://cdn.example.com/style.css")!)) + } + + @Test("uses https as default scheme when none specified") + func usesHttpsAsDefaultScheme() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.contains(URL(string: "https://cdn.example.com/script.js")!)) + } + + @Test("ignores inline scripts without src") + func ignoresInlineScripts() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.count == 1) + #expect(links.contains(URL(string: "https://example.com/app.js")!)) + } + + @Test("ignores link tags without stylesheet rel") + func ignoresNonStylesheetLinks() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.count == 1) + #expect(links.contains(URL(string: "https://example.com/style.css")!)) + } + + @Test("returns empty array for empty scripts and styles") + func returnsEmptyForEmptyContent() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let manifest = try LocalEditorAssetManifest.from(data: Data(json.utf8)) + let links = manifest.assetUrls + #expect(links.isEmpty) + } + + @Test("Empty Manifest is Empty") + func emptyManifestIsEmpty() throws { + #expect(LocalEditorAssetManifest.empty.scripts.isEmpty) + #expect(LocalEditorAssetManifest.empty.styles.isEmpty) + #expect(LocalEditorAssetManifest.empty.allowedBlockTypes.isEmpty) + #expect(LocalEditorAssetManifest.empty.rawStyles.isEmpty) + #expect(LocalEditorAssetManifest.empty.rawScripts.isEmpty) + #expect(LocalEditorAssetManifest.empty.assetUrls.isEmpty) + } +} + +extension LocalEditorAssetManifest { + fileprivate static func from(data: Data) throws -> LocalEditorAssetManifest { + let remote = try RemoteEditorAssetManifest(data: data) + return try LocalEditorAssetManifest(remoteManifest: remote) + } +} diff --git a/ios/Tests/GutenbergKitTests/Model/RemoteEditorAssetManifestTests.swift b/ios/Tests/GutenbergKitTests/Model/RemoteEditorAssetManifestTests.swift new file mode 100644 index 00000000..ac2f3167 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Model/RemoteEditorAssetManifestTests.swift @@ -0,0 +1,49 @@ +import Foundation +import Testing + +@testable import GutenbergKit + +@Suite +struct RemoteEditorAssetManifestTests { + + // MARK: - Decoding + @Test("decodes from valid JSON") + func decodesFromValidJSON() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": ["core/paragraph", "core/heading"] + } + """ + let data = Data(json.utf8) + let manifest = try RemoteEditorAssetManifest(data: data) + #expect(manifest.scripts.contains("script.js")) + #expect(manifest.styles.contains("style.css")) + #expect(manifest.allowedBlockTypes == ["core/paragraph", "core/heading"]) + } + + @Test("decodes empty allowed block types") + func decodesEmptyAllowedBlockTypes() throws { + let json = """ + { + "scripts": "", + "styles": "", + "allowed_block_types": [] + } + """ + let data = Data(json.utf8) + let manifest = try RemoteEditorAssetManifest(data: data) + #expect(manifest.allowedBlockTypes.isEmpty) + } + + // MARK: - Decoding + @Test( + "Successfully decodes test cases", + arguments: [ + "editor-asset-manifest-test-case-1" + ]) + func testCases(name: String) async throws { + _ = try RemoteEditorAssetManifest(data: Data.forResource(named: name)) + } +} diff --git a/ios/Tests/GutenbergKitTests/PreloaderProgressTests.swift b/ios/Tests/GutenbergKitTests/PreloaderProgressTests.swift new file mode 100644 index 00000000..cb2ed78c --- /dev/null +++ b/ios/Tests/GutenbergKitTests/PreloaderProgressTests.swift @@ -0,0 +1,22 @@ +import Testing + +@testable import GutenbergKit + +struct EditorProgressTests { + + @Test func `reports zero when there are no completed items`() { + #expect(EditorProgress(completed: 0, total: 5).fractionCompleted == 0) + } + + @Test func `reports zero when there are no total items`() { + #expect(EditorProgress(completed: 5, total: 0).fractionCompleted == 0) + } + + @Test func `reports the correct percentage when there are both completed and total items`() { + #expect(EditorProgress(completed: 5, total: 5).fractionCompleted == 1.0) + } + + @Test func `reports a maximum of 1.0 when there are more completed items than total items`() { + #expect(EditorProgress(completed: 10, total: 5).fractionCompleted == 1.0) + } +} diff --git a/ios/Tests/GutenbergKitTests/Resources/editor-asset-manifest-test-case-1.json b/ios/Tests/GutenbergKitTests/Resources/editor-asset-manifest-test-case-1.json new file mode 100644 index 00000000..54aa8a90 --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Resources/editor-asset-manifest-test-case-1.json @@ -0,0 +1,211 @@ +{ + "allowed_block_types": [ + "core/accordion-heading", + "core/accordion-panel", + "core/audio", + "core/buttons", + "core/code", + "core/column", + "core/columns", + "core/details", + "core/embed", + "core/form-submit-button", + "core/group", + "core/html", + "core/list-item", + "core/math", + "core/missing", + "core/more", + "core/nextpage", + "core/paragraph", + "core/preformatted", + "core/pullquote", + "core/quote", + "core/separator", + "core/social-links", + "core/spacer", + "core/table", + "core/terms-query", + "core/text-columns", + "core/verse", + "core/widget-area", + "core/social-link-amazon", + "core/social-link-bandcamp", + "core/social-link-behance", + "core/social-link-chain", + "core/social-link-codepen", + "core/social-link-deviantart", + "core/social-link-dribbble", + "core/social-link-dropbox", + "core/social-link-etsy", + "core/social-link-facebook", + "core/social-link-feed", + "core/social-link-fivehundredpx", + "core/social-link-flickr", + "core/social-link-foursquare", + "core/social-link-goodreads", + "core/social-link-google", + "core/social-link-github", + "core/social-link-instagram", + "core/social-link-lastfm", + "core/social-link-linkedin", + "core/social-link-mail", + "core/social-link-mastodon", + "core/social-link-meetup", + "core/social-link-medium", + "core/social-link-pinterest", + "core/social-link-pocket", + "core/social-link-reddit", + "core/social-link-skype", + "core/social-link-snapchat", + "core/social-link-soundcloud", + "core/social-link-spotify", + "core/social-link-tumblr", + "core/social-link-twitch", + "core/social-link-twitter", + "core/social-link-vimeo", + "core/social-link-vk", + "core/social-link-wordpress", + "core/social-link-yelp", + "core/social-link-youtube", + "core/video", + "core/accordion-item", + "core/accordion", + "core/archives", + "core/avatar", + "core/block", + "core/breadcrumbs", + "core/button", + "core/calendar", + "core/categories", + "core/comment-author-avatar", + "core/comment-author-name", + "core/comment-content", + "core/comment-date", + "core/comment-edit-link", + "core/comment-reply-link", + "core/comment-template", + "core/comments-pagination-next", + "core/comments-pagination-numbers", + "core/comments-pagination-previous", + "core/comments-pagination", + "core/comments-title", + "core/comments", + "core/cover", + "core/file", + "core/footnotes", + "core/gallery", + "core/heading", + "core/home-link", + "core/image", + "core/latest-comments", + "core/latest-posts", + "core/list", + "core/loginout", + "core/media-text", + "core/navigation-link", + "core/navigation-submenu", + "core/navigation", + "core/page-list-item", + "core/page-list", + "core/pattern", + "core/post-author-biography", + "core/post-author-name", + "core/post-author", + "core/post-comment", + "core/post-comments-count", + "core/post-comments-form", + "core/post-comments-link", + "core/post-content", + "core/post-date", + "core/post-excerpt", + "core/post-featured-image", + "core/post-navigation-link", + "core/post-template", + "core/post-terms", + "core/post-time-to-read", + "core/post-title", + "core/query-no-results", + "core/query-pagination-next", + "core/query-pagination-numbers", + "core/query-pagination-previous", + "core/query-pagination", + "core/query-title", + "core/query-total", + "core/query", + "core/read-more", + "core/rss", + "core/search", + "core/shortcode", + "core/site-logo", + "core/site-tagline", + "core/site-title", + "core/social-link", + "core/tab", + "core/table-of-contents", + "core/tabs", + "core/tag-cloud", + "core/template-part", + "core/term-count", + "core/term-description", + "core/term-name", + "core/term-template", + "core/legacy-widget", + "core/widget-group", + "core/post-comments", + "a8c/blog-posts", + "a8c/posts-carousel", + "jetpack/address", + "jetpack/ai-assistant", + "jetpack/blog-stats", + "jetpack/blogging-prompt", + "jetpack/blogroll", + "jetpack/blogroll-item", + "jetpack/business-hours", + "jetpack/button", + "jetpack/calendly", + "jetpack/contact-info", + "jetpack/email", + "jetpack/event-countdown", + "jetpack/eventbrite", + "jetpack/gif", + "jetpack/goodreads", + "jetpack/google-calendar", + "jetpack/image-compare", + "jetpack/instagram-gallery", + "jetpack/like", + "jetpack/mailchimp", + "jetpack/map", + "jetpack/markdown", + "jetpack/nextdoor", + "jetpack/opentable", + "jetpack/payment-buttons", + "jetpack/payments-intro", + "jetpack/paypal-payment-buttons", + "jetpack/phone", + "jetpack/pinterest", + "jetpack/podcast-player", + "jetpack/rating-star", + "jetpack/recurring-payments", + "jetpack/related-posts", + "jetpack/repeat-visitor", + "jetpack/send-a-message", + "jetpack/sharing-button", + "jetpack/sharing-buttons", + "jetpack/simple-payments", + "jetpack/subscriber-login", + "jetpack/subscriptions", + "jetpack/tiled-gallery", + "jetpack/timeline", + "jetpack/timeline-item", + "jetpack/top-posts", + "jetpack/whatsapp-button", + "premium-content/buttons", + "premium-content/container", + "premium-content/logged-out-view", + "premium-content/login-button", + "premium-content/subscriber-view" + ], + "scripts": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "styles": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +} diff --git a/ios/Tests/GutenbergKitTests/Resources/editor-settings-test-case-1.json b/ios/Tests/GutenbergKitTests/Resources/editor-settings-test-case-1.json new file mode 100644 index 00000000..08c5f77b --- /dev/null +++ b/ios/Tests/GutenbergKitTests/Resources/editor-settings-test-case-1.json @@ -0,0 +1,1179 @@ +{ + "alignWide": false, + "allowedBlockTypes": true, + "allowedMimeTypes": { + "jpg|jpeg|jpe": "image/jpeg", + "gif": "image/gif", + "png": "image/png", + "bmp": "image/bmp", + "tiff|tif": "image/tiff", + "webp": "image/webp", + "avif": "image/avif", + "ico": "image/x-icon", + "heic": "image/heic", + "heif": "image/heif", + "heics": "image/heic-sequence", + "heifs": "image/heif-sequence", + "asf|asx": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "wmx": "video/x-ms-wmx", + "wm": "video/x-ms-wm", + "avi": "video/avi", + "divx": "video/divx", + "flv": "video/x-flv", + "mov|qt": "video/quicktime", + "mpeg|mpg|mpe": "video/mpeg", + "mp4|m4v": "video/mp4", + "ogv": "video/ogg", + "webm": "video/webm", + "mkv": "video/x-matroska", + "3gp|3gpp": "video/3gpp", + "3g2|3gp2": "video/3gpp2", + "txt|asc|c|cc|h|srt": "text/plain", + "csv": "text/csv", + "tsv": "text/tab-separated-values", + "ics": "text/calendar", + "rtx": "text/richtext", + "css": "text/css", + "htm|html": "text/html", + "vtt": "text/vtt", + "dfxp": "application/ttaf+xml", + "mp3|m4a|m4b": "audio/mpeg", + "aac": "audio/aac", + "ra|ram": "audio/x-realaudio", + "wav|x-wav": "audio/wav", + "ogg|oga": "audio/ogg", + "flac": "audio/flac", + "mid|midi": "audio/midi", + "wma": "audio/x-ms-wma", + "wax": "audio/x-ms-wax", + "mka": "audio/x-matroska", + "rtf": "application/rtf", + "js": "application/javascript", + "pdf": "application/pdf", + "class": "application/java", + "tar": "application/x-tar", + "zip": "application/zip", + "gz|gzip": "application/x-gzip", + "rar": "application/rar", + "7z": "application/x-7z-compressed", + "psd": "application/octet-stream", + "xcf": "application/octet-stream", + "doc": "application/msword", + "pot|pps|ppt": "application/vnd.ms-powerpoint", + "wri": "application/vnd.ms-write", + "xla|xls|xlt|xlw": "application/vnd.ms-excel", + "mdb": "application/vnd.ms-access", + "mpp": "application/vnd.ms-project", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docm": "application/vnd.ms-word.document.macroEnabled.12", + "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "dotm": "application/vnd.ms-word.template.macroEnabled.12", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12", + "xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xltm": "application/vnd.ms-excel.template.macroEnabled.12", + "xlam": "application/vnd.ms-excel.addin.macroEnabled.12", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + "potx": "application/vnd.openxmlformats-officedocument.presentationml.template", + "potm": "application/vnd.ms-powerpoint.template.macroEnabled.12", + "ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12", + "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", + "sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12", + "onetoc|onetoc2|onetmp|onepkg": "application/onenote", + "oxps": "application/oxps", + "xps": "application/vnd.ms-xpsdocument", + "odt": "application/vnd.oasis.opendocument.text", + "odp": "application/vnd.oasis.opendocument.presentation", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odg": "application/vnd.oasis.opendocument.graphics", + "odc": "application/vnd.oasis.opendocument.chart", + "odb": "application/vnd.oasis.opendocument.database", + "odf": "application/vnd.oasis.opendocument.formula", + "wp|wpd": "application/wordperfect", + "key": "application/vnd.apple.keynote", + "numbers": "application/vnd.apple.numbers", + "pages": "application/vnd.apple.pages", + "videopress": "video/videopress" + }, + "defaultEditorStyles": [ + { + "css": "body{\n font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;\n font-size:18px;\n line-height:1.5;\n --wp--style--block-gap:2em;\n}\n\np{\n line-height:1.8;\n}\n\n.editor-post-title__block{\n font-size:2.5em;\n font-weight:800;\n margin-bottom:1em;\n margin-top:2em;\n}" + } + ], + "blockCategories": [ + { + "slug": "text", + "title": "Text", + "icon": null + }, + { + "slug": "media", + "title": "Media", + "icon": null + }, + { + "slug": "design", + "title": "Design", + "icon": null + }, + { + "slug": "widgets", + "title": "Widgets", + "icon": null + }, + { + "slug": "theme", + "title": "Theme", + "icon": null + }, + { + "slug": "embed", + "title": "Embeds", + "icon": null + }, + { + "slug": "reusable", + "title": "Patterns", + "icon": null + } + ], + "isRTL": false, + "imageDefaultSize": "large", + "imageDimensions": { + "thumbnail": { + "width": 150, + "height": 150, + "crop": true + }, + "medium": { + "width": 300, + "height": 300, + "crop": false + }, + "large": { + "width": 1024, + "height": 1024, + "crop": false + } + }, + "imageEditing": true, + "imageSizes": [ + { + "slug": "thumbnail", + "name": "Thumbnail" + }, + { + "slug": "medium", + "name": "Medium" + }, + { + "slug": "large", + "name": "Large" + }, + { + "slug": "full", + "name": "Full Size" + } + ], + "maxUploadFileSize": 1310720000, + "__experimentalDashboardLink": "https://example.wordpress.test/wp-admin/", + "__unstableGalleryWithImageBlocks": true, + "disableCustomColors": false, + "disableCustomFontSizes": false, + "disableCustomGradients": false, + "disableLayoutStyles": false, + "enableCustomLineHeight": true, + "enableCustomSpacing": true, + "enableCustomUnits": [ + "%", + "px", + "em", + "rem", + "vh", + "vw" + ], + "__experimentalBlockBindingsSupportedAttributes": { + "core/paragraph": [ + "content" + ], + "core/button": [ + "url", + "text", + "linkTarget", + "rel" + ], + "core/heading": [ + "content" + ], + "core/image": [ + "id", + "url", + "title", + "alt", + "caption" + ], + "core/navigation-link": [ + "url" + ], + "core/navigation-submenu": [ + "url" + ], + "core/post-date": [ + "datetime" + ] + }, + "styles": [ + { + "css": ":root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--color--base: #f9f9f9;--wp--preset--color--base-2: #ffffff;--wp--preset--color--contrast: #111111;--wp--preset--color--contrast-2: #636363;--wp--preset--color--contrast-3: #A4A4A4;--wp--preset--color--accent: #cfcabe;--wp--preset--color--accent-2: #c2a990;--wp--preset--color--accent-3: #d8613c;--wp--preset--color--accent-4: #b1c5a4;--wp--preset--color--accent-5: #b5bdbc;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgb(252,185,0) 0%,rgb(255,105,0) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgb(255,105,0) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--gradient--gradient-1: linear-gradient(to bottom, #cfcabe 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-2: linear-gradient(to bottom, #C2A990 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-3: linear-gradient(to bottom, #D8613C 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-4: linear-gradient(to bottom, #B1C5A4 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-5: linear-gradient(to bottom, #B5BDBC 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-6: linear-gradient(to bottom, #A4A4A4 0%, #F9F9F9 100%);--wp--preset--gradient--gradient-7: linear-gradient(to bottom, #cfcabe 50%, #F9F9F9 50%);--wp--preset--gradient--gradient-8: linear-gradient(to bottom, #C2A990 50%, #F9F9F9 50%);--wp--preset--gradient--gradient-9: linear-gradient(to bottom, #D8613C 50%, #F9F9F9 50%);--wp--preset--gradient--gradient-10: linear-gradient(to bottom, #B1C5A4 50%, #F9F9F9 50%);--wp--preset--gradient--gradient-11: linear-gradient(to bottom, #B5BDBC 50%, #F9F9F9 50%);--wp--preset--gradient--gradient-12: linear-gradient(to bottom, #A4A4A4 50%, #F9F9F9 50%);--wp--preset--font-size--small: 0.9rem;--wp--preset--font-size--medium: 1.05rem;--wp--preset--font-size--large: clamp(1.39rem, 1.39rem + ((1vw - 0.2rem) * 0.767), 1.85rem);--wp--preset--font-size--x-large: clamp(1.85rem, 1.85rem + ((1vw - 0.2rem) * 1.083), 2.5rem);--wp--preset--font-size--xx-large: clamp(2.5rem, 2.5rem + ((1vw - 0.2rem) * 1.283), 3.27rem);--wp--preset--font-family--body: \"Inter\", sans-serif;--wp--preset--font-family--heading: Cardo;--wp--preset--font-family--system-sans-serif: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;--wp--preset--font-family--system-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--wp--preset--spacing--20: min(1.5rem, 2vw);--wp--preset--spacing--30: min(2.5rem, 3vw);--wp--preset--spacing--40: min(4rem, 5vw);--wp--preset--spacing--50: min(6.5rem, 8vw);--wp--preset--spacing--60: min(10.5rem, 13vw);--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--spacing--10: 1rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgb(255, 255, 255), 6px 6px rgb(0, 0, 0);--wp--preset--shadow--crisp: 6px 6px 0px rgb(0, 0, 0);}", + "__unstableType": "presets", + "isGlobalStyles": true + }, + { + "css": ".has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-base-color{color: var(--wp--preset--color--base) !important;}.has-base-2-color{color: var(--wp--preset--color--base-2) !important;}.has-contrast-color{color: var(--wp--preset--color--contrast) !important;}.has-contrast-2-color{color: var(--wp--preset--color--contrast-2) !important;}.has-contrast-3-color{color: var(--wp--preset--color--contrast-3) !important;}.has-accent-color{color: var(--wp--preset--color--accent) !important;}.has-accent-2-color{color: var(--wp--preset--color--accent-2) !important;}.has-accent-3-color{color: var(--wp--preset--color--accent-3) !important;}.has-accent-4-color{color: var(--wp--preset--color--accent-4) !important;}.has-accent-5-color{color: var(--wp--preset--color--accent-5) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-base-background-color{background-color: var(--wp--preset--color--base) !important;}.has-base-2-background-color{background-color: var(--wp--preset--color--base-2) !important;}.has-contrast-background-color{background-color: var(--wp--preset--color--contrast) !important;}.has-contrast-2-background-color{background-color: var(--wp--preset--color--contrast-2) !important;}.has-contrast-3-background-color{background-color: var(--wp--preset--color--contrast-3) !important;}.has-accent-background-color{background-color: var(--wp--preset--color--accent) !important;}.has-accent-2-background-color{background-color: var(--wp--preset--color--accent-2) !important;}.has-accent-3-background-color{background-color: var(--wp--preset--color--accent-3) !important;}.has-accent-4-background-color{background-color: var(--wp--preset--color--accent-4) !important;}.has-accent-5-background-color{background-color: var(--wp--preset--color--accent-5) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-base-border-color{border-color: var(--wp--preset--color--base) !important;}.has-base-2-border-color{border-color: var(--wp--preset--color--base-2) !important;}.has-contrast-border-color{border-color: var(--wp--preset--color--contrast) !important;}.has-contrast-2-border-color{border-color: var(--wp--preset--color--contrast-2) !important;}.has-contrast-3-border-color{border-color: var(--wp--preset--color--contrast-3) !important;}.has-accent-border-color{border-color: var(--wp--preset--color--accent) !important;}.has-accent-2-border-color{border-color: var(--wp--preset--color--accent-2) !important;}.has-accent-3-border-color{border-color: var(--wp--preset--color--accent-3) !important;}.has-accent-4-border-color{border-color: var(--wp--preset--color--accent-4) !important;}.has-accent-5-border-color{border-color: var(--wp--preset--color--accent-5) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-gradient-1-gradient-background{background: var(--wp--preset--gradient--gradient-1) !important;}.has-gradient-2-gradient-background{background: var(--wp--preset--gradient--gradient-2) !important;}.has-gradient-3-gradient-background{background: var(--wp--preset--gradient--gradient-3) !important;}.has-gradient-4-gradient-background{background: var(--wp--preset--gradient--gradient-4) !important;}.has-gradient-5-gradient-background{background: var(--wp--preset--gradient--gradient-5) !important;}.has-gradient-6-gradient-background{background: var(--wp--preset--gradient--gradient-6) !important;}.has-gradient-7-gradient-background{background: var(--wp--preset--gradient--gradient-7) !important;}.has-gradient-8-gradient-background{background: var(--wp--preset--gradient--gradient-8) !important;}.has-gradient-9-gradient-background{background: var(--wp--preset--gradient--gradient-9) !important;}.has-gradient-10-gradient-background{background: var(--wp--preset--gradient--gradient-10) !important;}.has-gradient-11-gradient-background{background: var(--wp--preset--gradient--gradient-11) !important;}.has-gradient-12-gradient-background{background: var(--wp--preset--gradient--gradient-12) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}.has-xx-large-font-size{font-size: var(--wp--preset--font-size--xx-large) !important;}.has-body-font-family{font-family: var(--wp--preset--font-family--body) !important;}.has-heading-font-family{font-family: var(--wp--preset--font-family--heading) !important;}.has-system-sans-serif-font-family{font-family: var(--wp--preset--font-family--system-sans-serif) !important;}.has-system-serif-font-family{font-family: var(--wp--preset--font-family--system-serif) !important;}", + "__unstableType": "presets", + "isGlobalStyles": true + }, + { + "css": ":root { --wp--style--global--content-size: 620px;--wp--style--global--wide-size: 1280px; }:where(body) { margin: 0; }.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1.2rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1.2rem; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1.2rem;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1.2rem;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1.2rem;}:root :where(.is-layout-grid){gap: 1.2rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}body{background-color: var(--wp--preset--color--base);color: var(--wp--preset--color--contrast);font-family: var(--wp--preset--font-family--body);font-size: var(--wp--preset--font-size--medium);font-style: normal;font-weight: 400;line-height: 1.55;--wp--style--root--padding-top: 0px;--wp--style--root--padding-right: var(--wp--preset--spacing--50);--wp--style--root--padding-bottom: 0px;--wp--style--root--padding-left: var(--wp--preset--spacing--50);}a:where(:not(.wp-element-button)){color: var(--wp--preset--color--contrast);text-decoration: underline;}:root :where(a:where(:not(.wp-element-button)):hover){text-decoration: none;}h1, h2, h3, h4, h5, h6{color: var(--wp--preset--color--contrast);font-family: var(--wp--preset--font-family--heading);font-weight: 400;line-height: 1.2;}h1{font-size: var(--wp--preset--font-size--xx-large);line-height: 1.15;}h2{font-size: var(--wp--preset--font-size--x-large);}h3{font-size: var(--wp--preset--font-size--large);}h4{font-size: clamp(1.1rem, 1.1rem + ((1vw - 0.2rem) * 0.767), 1.5rem);}h5{font-size: var(--wp--preset--font-size--medium);}h6{font-size: var(--wp--preset--font-size--small);}:root :where(.wp-element-button, .wp-block-button__link){background-color: var(--wp--preset--color--contrast);border-radius: .33rem;border-color: var(--wp--preset--color--contrast);border-width: 0;color: var(--wp--preset--color--base);font-family: inherit;font-size: var(--wp--preset--font-size--small);font-style: normal;font-weight: 500;letter-spacing: inherit;line-height: inherit;padding-top: 0.6rem;padding-right: 1rem;padding-bottom: 0.6rem;padding-left: 1rem;text-decoration: none;text-transform: inherit;}:root :where(.wp-element-button:hover, .wp-block-button__link:hover){background-color: var(--wp--preset--color--contrast-2);border-color: var(--wp--preset--color--contrast-2);color: var(--wp--preset--color--base);}:root :where(.wp-element-button:focus, .wp-block-button__link:focus){background-color: var(--wp--preset--color--contrast-2);border-color: var(--wp--preset--color--contrast-2);color: var(--wp--preset--color--base);outline-color: var(--wp--preset--color--contrast);outline-offset: 2px;}:root :where(.wp-element-button:active, .wp-block-button__link:active){background-color: var(--wp--preset--color--contrast);color: var(--wp--preset--color--base);}:root :where(.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption){color: var(--wp--preset--color--contrast-2);font-family: var(--wp--preset--font-family--body);font-size: 0.8rem;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link){background: transparent none;border-color: currentColor;border-width: 1px;border-style: solid;color: currentColor;padding-top: calc(0.6rem - 1px);padding-right: calc(1rem - 1px);padding-bottom: calc(0.6rem - 1px);padding-left: calc(1rem - 1px);}:root :where(.wp-block-site-logo.is-style-rounded){border-radius: 9999px;}:root :where(.wp-block-pullquote){border-radius: var(--wp--preset--spacing--20);font-family: var(--wp--preset--font-family--heading);font-size: var(--wp--preset--font-size--x-large);font-style: italic;font-weight: 400;letter-spacing: 0em;line-height: 1.5;padding-top: var(--wp--preset--spacing--40);padding-bottom: var(--wp--preset--spacing--40);}:root :where(.wp-block-pullquote cite){font-family: var(--wp--preset--font-family--body);font-size: var(--wp--preset--font-size--medium);font-style: normal;}:root :where(.wp-block-avatar img){border-radius: 90px;}:root :where(.wp-block-buttons-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flow) > *{margin-block-start: 0.7rem;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > *{margin-block-start: 0.7rem;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flex){gap: 0.7rem;}:root :where(.wp-block-buttons-is-layout-grid){gap: 0.7rem;}:root :where(.wp-block-calendar table, .wp-block-calendar th){color: var(--wp--preset--color--contrast);}:root :where(.wp-block-calendar.wp-block-calendar table:where(:not(.has-text-color)) th){background-color:var(--wp--preset--color--contrast-2);color:var(--wp--preset--color--base);border-color:var(--wp--preset--color--contrast-2)}:root :where(.wp-block-calendar table:where(:not(.has-text-color)) td){border-color:var(--wp--preset--color--contrast-2)}:root :where(.wp-block-categories){padding-right: 0px;padding-left: 0px;}:root :where(.wp-block-categories){list-style-type:none;}:root :where(.wp-block-categories li){margin-bottom: 0.5rem;}:root :where(.wp-block-code){background-color: var(--wp--preset--color--base-2);border-radius: var(--wp--preset--spacing--20);border-color: var(--wp--preset--color--contrast);color: var(--wp--preset--color--contrast-2);font-size: var(--wp--preset--font-size--medium);font-style: normal;font-weight: 400;line-height: 1.6;padding-top: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-right: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-bottom: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-left: calc(var(--wp--preset--spacing--30) + 0.75rem);}:root :where(.wp-block-comment-author-name){color: var(--wp--preset--color--contrast);font-size: var(--wp--preset--font-size--small);font-style: normal;font-weight: 600;}:root :where(.wp-block-comment-author-name a:where(:not(.wp-element-button))){color: var(--wp--preset--color--contrast);text-decoration: none;}:root :where(.wp-block-comment-author-name a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-comment-content){font-size: var(--wp--preset--font-size--small);margin-top: var(--wp--preset--spacing--20);margin-bottom: var(--wp--preset--spacing--20);}:root :where(.wp-block-comment-date){color: var(--wp--preset--color--contrast-2);font-size: var(--wp--preset--font-size--small);margin-top: 0px;margin-bottom: 0px;}:root :where(.wp-block-comment-date a:where(:not(.wp-element-button))){color: var(--wp--preset--color--contrast-2);text-decoration: none;}:root :where(.wp-block-comment-date a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-comment-edit-link){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-comment-edit-link a:where(:not(.wp-element-button))){color: var(--wp--preset--color--contrast-2);text-decoration: none;}:root :where(.wp-block-comment-edit-link a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-comment-reply-link){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-comment-reply-link a:where(:not(.wp-element-button))){color: var(--wp--preset--color--contrast-2);text-decoration: none;}:root :where(.wp-block-comment-reply-link a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-post-comments-form textarea, .wp-block-post-comments-form input){border-radius:.33rem}:root :where(.wp-block-comments-pagination){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-comments-pagination-next){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-comments-pagination-numbers){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-comments-pagination-previous){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-footnotes){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-gallery){margin-bottom: var(--wp--preset--spacing--50);}:root :where(.wp-block-image.is-style-rounded img,.wp-block-image.is-style-rounded __crop-area,.wp-block-image.is-style-rounded .components-placeholder){border-radius: var(--wp--preset--spacing--20);}:root :where(.wp-block-list){padding-left: var(--wp--preset--spacing--10);}:root :where(.wp-block-loginout input){border-radius:.33rem;padding:calc(0.667em + 2px);border:1px solid #949494;}:root :where(.wp-block-navigation){font-weight: 500;}:root :where(.wp-block-navigation a:where(:not(.wp-element-button))){text-decoration: none;}:root :where(.wp-block-navigation a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-post-author){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-post-author-name){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-post-author-name a:where(:not(.wp-element-button))){text-decoration: none;}:root :where(.wp-block-post-author-name a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-post-date){color: var(--wp--preset--color--contrast-2);font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){color: var(--wp--preset--color--contrast-2);text-decoration: none;}:root :where(.wp-block-post-date a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-post-excerpt){line-height: 1.6;}:root :where(.wp-block-post-featured-image img, .wp-block-post-featured-image .block-editor-media-placeholder, .wp-block-post-featured-image .wp-block-post-featured-image__overlay){border-radius: var(--wp--preset--spacing--20);}:root :where(.wp-block-post-terms){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-post-terms .wp-block-post-terms__prefix){color: var(--wp--preset--color--contrast-2);}:root :where(.wp-block-post-terms a:where(:not(.wp-element-button))){text-decoration: none;}:root :where(.wp-block-post-terms a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-post-title a:where(:not(.wp-element-button))){text-decoration: none;}:root :where(.wp-block-post-title a:where(:not(.wp-element-button)):hover){text-decoration: underline;}:root :where(.wp-block-query-title span){font-style: italic;}:root :where(.wp-block-query-no-results){padding-top: var(--wp--preset--spacing--30);}:root :where(.wp-block-quote){background-color: var(--wp--preset--color--base-2);border-radius: var(--wp--preset--spacing--20);font-family: var(--wp--preset--font-family--heading);font-size: var(--wp--preset--font-size--large);font-style: italic;line-height: 1.3;padding-top: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-right: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-bottom: calc(var(--wp--preset--spacing--30) + 0.75rem);padding-left: calc(var(--wp--preset--spacing--30) + 0.75rem);}:root :where(.wp-block-quote.is-style-plain){background-color: transparent;border-radius: 0;border-color: var(--wp--preset--color--contrast);border-width: 0;border-style: solid;font-family: var(--wp--preset--font-family--body);font-size: var(--wp--preset--font-size--medium);font-style: normal;line-height: 1.5;padding-top: var(--wp--preset--spacing--20);padding-right: var(--wp--preset--spacing--20);padding-bottom: var(--wp--preset--spacing--20);padding-left: var(--wp--preset--spacing--20);}:root :where(.wp-block-quote :where(p)){margin-block-start:0;margin-block-end:calc(var(--wp--preset--spacing--10) + 0.5rem);}:root :where(.wp-block-quote :where(:last-child)){margin-block-end:0;}:root :where(.wp-block-quote.has-text-align-right.is-style-plain, .rtl .is-style-plain.wp-block-quote:not(.has-text-align-center):not(.has-text-align-left)){border-width: 0 2px 0 0;padding-left:calc(var(--wp--preset--spacing--20) + 0.5rem);padding-right:calc(var(--wp--preset--spacing--20) + 0.5rem);}:root :where(.wp-block-quote.has-text-align-left.is-style-plain, body:not(.rtl) .is-style-plain.wp-block-quote:not(.has-text-align-center):not(.has-text-align-right)){border-width: 0 0 0 2px;padding-left:calc(var(--wp--preset--spacing--20) + 0.5rem);padding-right:calc(var(--wp--preset--spacing--20) + 0.5rem)}:root :where(.wp-block-quote cite){font-family: var(--wp--preset--font-family--body);font-size: var(--wp--preset--font-size--small);font-style: normal;}:root :where(.wp-block-search .wp-block-search__label, .wp-block-search .wp-block-search__input, .wp-block-search .wp-block-search__button){font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-search .wp-block-search__input){border-radius:.33rem}:root :where(.wp-block-search .wp-element-button,.wp-block-search .wp-block-button__link){border-radius: .33rem;}:root :where(.wp-block-separator){border-color: currentColor;border-width: 0 0 1px 0;border-style: solid;color: var(--wp--preset--color--contrast);}:root :where(.wp-block-separator){}:root :where(.wp-block-separator:not(.is-style-wide):not(.is-style-dots):not(.alignwide):not(.alignfull)){width: var(--wp--preset--spacing--60)}:root :where(.wp-block-site-tagline){color: var(--wp--preset--color--contrast-2);font-size: var(--wp--preset--font-size--small);}:root :where(.wp-block-site-title){font-family: var(--wp--preset--font-family--body);font-size: clamp(0.875rem, 0.875rem + ((1vw - 0.2rem) * 0.542), 1.2rem);font-style: normal;font-weight: 600;}:root :where(.wp-block-site-title a:where(:not(.wp-element-button))){text-decoration: none;}:root :where(.wp-block-site-title a:where(:not(.wp-element-button)):hover){text-decoration: none;}", + "__unstableType": "theme", + "isGlobalStyles": true + }, + { + "css": "", + "__unstableType": "user", + "isGlobalStyles": false + }, + { + "css": ":where(.wp-site-blocks *:focus){outline-width:2px;outline-style:solid}", + "__unstableType": "user", + "isGlobalStyles": true + }, + { + "assets": "", + "__unstableType": "svgs", + "isGlobalStyles": false + }, + { + "css": ":root{--wp--preset--duotone--dark-grayscale:url(#wp-duotone-dark-grayscale);--wp--preset--duotone--grayscale:url(#wp-duotone-grayscale);--wp--preset--duotone--purple-yellow:url(#wp-duotone-purple-yellow);--wp--preset--duotone--blue-red:url(#wp-duotone-blue-red);--wp--preset--duotone--midnight:url(#wp-duotone-midnight);--wp--preset--duotone--magenta-yellow:url(#wp-duotone-magenta-yellow);--wp--preset--duotone--purple-green:url(#wp-duotone-purple-green);--wp--preset--duotone--blue-orange:url(#wp-duotone-blue-orange);--wp--preset--duotone--duotone-1:url(#wp-duotone-duotone-1);--wp--preset--duotone--duotone-2:url(#wp-duotone-duotone-2);--wp--preset--duotone--duotone-3:url(#wp-duotone-duotone-3);--wp--preset--duotone--duotone-4:url(#wp-duotone-duotone-4);--wp--preset--duotone--duotone-5:url(#wp-duotone-duotone-5);}", + "__unstableType": "presets", + "isGlobalStyles": false + } + ], + "__experimentalFeatures": { + "appearanceTools": false, + "useRootPaddingAwareAlignments": true, + "border": { + "color": true, + "radius": true, + "style": true, + "width": true + }, + "color": { + "background": true, + "button": true, + "caption": true, + "customDuotone": true, + "defaultDuotone": false, + "defaultGradients": false, + "defaultPalette": false, + "duotone": { + "default": [ + { + "name": "Dark grayscale", + "colors": [ + "#000000", + "#7f7f7f" + ], + "slug": "dark-grayscale" + }, + { + "name": "Grayscale", + "colors": [ + "#000000", + "#ffffff" + ], + "slug": "grayscale" + }, + { + "name": "Purple and yellow", + "colors": [ + "#8c00b7", + "#fcff41" + ], + "slug": "purple-yellow" + }, + { + "name": "Blue and red", + "colors": [ + "#000097", + "#ff4747" + ], + "slug": "blue-red" + }, + { + "name": "Midnight", + "colors": [ + "#000000", + "#00a5ff" + ], + "slug": "midnight" + }, + { + "name": "Magenta and yellow", + "colors": [ + "#c7005a", + "#fff278" + ], + "slug": "magenta-yellow" + }, + { + "name": "Purple and green", + "colors": [ + "#a60072", + "#67ff66" + ], + "slug": "purple-green" + }, + { + "name": "Blue and orange", + "colors": [ + "#1900d8", + "#ffa96b" + ], + "slug": "blue-orange" + } + ], + "theme": [ + { + "colors": [ + "#111111", + "#ffffff" + ], + "slug": "duotone-1", + "name": "Black and white" + }, + { + "colors": [ + "#111111", + "#C2A990" + ], + "slug": "duotone-2", + "name": "Black and sandstone" + }, + { + "colors": [ + "#111111", + "#D8613C" + ], + "slug": "duotone-3", + "name": "Black and rust" + }, + { + "colors": [ + "#111111", + "#B1C5A4" + ], + "slug": "duotone-4", + "name": "Black and sage" + }, + { + "colors": [ + "#111111", + "#B5BDBC" + ], + "slug": "duotone-5", + "name": "Black and pastel blue" + } + ] + }, + "gradients": { + "default": [ + { + "name": "Vivid cyan blue to vivid purple", + "gradient": "linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%)", + "slug": "vivid-cyan-blue-to-vivid-purple" + }, + { + "name": "Light green cyan to vivid green cyan", + "gradient": "linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)", + "slug": "light-green-cyan-to-vivid-green-cyan" + }, + { + "name": "Luminous vivid amber to luminous vivid orange", + "gradient": "linear-gradient(135deg,rgb(252,185,0) 0%,rgb(255,105,0) 100%)", + "slug": "luminous-vivid-amber-to-luminous-vivid-orange" + }, + { + "name": "Luminous vivid orange to vivid red", + "gradient": "linear-gradient(135deg,rgb(255,105,0) 0%,rgb(207,46,46) 100%)", + "slug": "luminous-vivid-orange-to-vivid-red" + }, + { + "name": "Very light gray to cyan bluish gray", + "gradient": "linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%)", + "slug": "very-light-gray-to-cyan-bluish-gray" + }, + { + "name": "Cool to warm spectrum", + "gradient": "linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%)", + "slug": "cool-to-warm-spectrum" + }, + { + "name": "Blush light purple", + "gradient": "linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%)", + "slug": "blush-light-purple" + }, + { + "name": "Blush bordeaux", + "gradient": "linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%)", + "slug": "blush-bordeaux" + }, + { + "name": "Luminous dusk", + "gradient": "linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%)", + "slug": "luminous-dusk" + }, + { + "name": "Pale ocean", + "gradient": "linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%)", + "slug": "pale-ocean" + }, + { + "name": "Electric grass", + "gradient": "linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%)", + "slug": "electric-grass" + }, + { + "name": "Midnight", + "gradient": "linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%)", + "slug": "midnight" + } + ], + "theme": [ + { + "slug": "gradient-1", + "gradient": "linear-gradient(to bottom, #cfcabe 0%, #F9F9F9 100%)", + "name": "Vertical soft beige to white" + }, + { + "slug": "gradient-2", + "gradient": "linear-gradient(to bottom, #C2A990 0%, #F9F9F9 100%)", + "name": "Vertical soft sandstone to white" + }, + { + "slug": "gradient-3", + "gradient": "linear-gradient(to bottom, #D8613C 0%, #F9F9F9 100%)", + "name": "Vertical soft rust to white" + }, + { + "slug": "gradient-4", + "gradient": "linear-gradient(to bottom, #B1C5A4 0%, #F9F9F9 100%)", + "name": "Vertical soft sage to white" + }, + { + "slug": "gradient-5", + "gradient": "linear-gradient(to bottom, #B5BDBC 0%, #F9F9F9 100%)", + "name": "Vertical soft mint to white" + }, + { + "slug": "gradient-6", + "gradient": "linear-gradient(to bottom, #A4A4A4 0%, #F9F9F9 100%)", + "name": "Vertical soft pewter to white" + }, + { + "slug": "gradient-7", + "gradient": "linear-gradient(to bottom, #cfcabe 50%, #F9F9F9 50%)", + "name": "Vertical hard beige to white" + }, + { + "slug": "gradient-8", + "gradient": "linear-gradient(to bottom, #C2A990 50%, #F9F9F9 50%)", + "name": "Vertical hard sandstone to white" + }, + { + "slug": "gradient-9", + "gradient": "linear-gradient(to bottom, #D8613C 50%, #F9F9F9 50%)", + "name": "Vertical hard rust to white" + }, + { + "slug": "gradient-10", + "gradient": "linear-gradient(to bottom, #B1C5A4 50%, #F9F9F9 50%)", + "name": "Vertical hard sage to white" + }, + { + "slug": "gradient-11", + "gradient": "linear-gradient(to bottom, #B5BDBC 50%, #F9F9F9 50%)", + "name": "Vertical hard mint to white" + }, + { + "slug": "gradient-12", + "gradient": "linear-gradient(to bottom, #A4A4A4 50%, #F9F9F9 50%)", + "name": "Vertical hard pewter to white" + } + ] + }, + "heading": true, + "link": true, + "palette": { + "default": [ + { + "name": "Black", + "slug": "black", + "color": "#000000" + }, + { + "name": "Cyan bluish gray", + "slug": "cyan-bluish-gray", + "color": "#abb8c3" + }, + { + "name": "White", + "slug": "white", + "color": "#ffffff" + }, + { + "name": "Pale pink", + "slug": "pale-pink", + "color": "#f78da7" + }, + { + "name": "Vivid red", + "slug": "vivid-red", + "color": "#cf2e2e" + }, + { + "name": "Luminous vivid orange", + "slug": "luminous-vivid-orange", + "color": "#ff6900" + }, + { + "name": "Luminous vivid amber", + "slug": "luminous-vivid-amber", + "color": "#fcb900" + }, + { + "name": "Light green cyan", + "slug": "light-green-cyan", + "color": "#7bdcb5" + }, + { + "name": "Vivid green cyan", + "slug": "vivid-green-cyan", + "color": "#00d084" + }, + { + "name": "Pale cyan blue", + "slug": "pale-cyan-blue", + "color": "#8ed1fc" + }, + { + "name": "Vivid cyan blue", + "slug": "vivid-cyan-blue", + "color": "#0693e3" + }, + { + "name": "Vivid purple", + "slug": "vivid-purple", + "color": "#9b51e0" + } + ], + "theme": [ + { + "color": "#f9f9f9", + "name": "Base", + "slug": "base" + }, + { + "color": "#ffffff", + "name": "Base / Two", + "slug": "base-2" + }, + { + "color": "#111111", + "name": "Contrast", + "slug": "contrast" + }, + { + "color": "#636363", + "name": "Contrast / Two", + "slug": "contrast-2" + }, + { + "color": "#A4A4A4", + "name": "Contrast / Three", + "slug": "contrast-3" + }, + { + "color": "#cfcabe", + "name": "Accent", + "slug": "accent" + }, + { + "color": "#c2a990", + "name": "Accent / Two", + "slug": "accent-2" + }, + { + "color": "#d8613c", + "name": "Accent / Three", + "slug": "accent-3" + }, + { + "color": "#b1c5a4", + "name": "Accent / Four", + "slug": "accent-4" + }, + { + "color": "#b5bdbc", + "name": "Accent / Five", + "slug": "accent-5" + } + ] + }, + "text": true + }, + "dimensions": { + "defaultAspectRatios": true, + "aspectRatios": { + "default": [ + { + "name": "Square - 1:1", + "slug": "square", + "ratio": "1" + }, + { + "name": "Standard - 4:3", + "slug": "4-3", + "ratio": "4/3" + }, + { + "name": "Portrait - 3:4", + "slug": "3-4", + "ratio": "3/4" + }, + { + "name": "Classic - 3:2", + "slug": "3-2", + "ratio": "3/2" + }, + { + "name": "Classic Portrait - 2:3", + "slug": "2-3", + "ratio": "2/3" + }, + { + "name": "Wide - 16:9", + "slug": "16-9", + "ratio": "16/9" + }, + { + "name": "Tall - 9:16", + "slug": "9-16", + "ratio": "9/16" + } + ] + }, + "aspectRatio": true, + "minHeight": true, + "width": true + }, + "shadow": { + "defaultPresets": true, + "presets": { + "default": [ + { + "name": "Natural", + "slug": "natural", + "shadow": "6px 6px 9px rgba(0, 0, 0, 0.2)" + }, + { + "name": "Deep", + "slug": "deep", + "shadow": "12px 12px 50px rgba(0, 0, 0, 0.4)" + }, + { + "name": "Sharp", + "slug": "sharp", + "shadow": "6px 6px 0px rgba(0, 0, 0, 0.2)" + }, + { + "name": "Outlined", + "slug": "outlined", + "shadow": "6px 6px 0px -3px rgb(255, 255, 255), 6px 6px rgb(0, 0, 0)" + }, + { + "name": "Crisp", + "slug": "crisp", + "shadow": "6px 6px 0px rgb(0, 0, 0)" + } + ] + } + }, + "spacing": { + "blockGap": true, + "margin": true, + "defaultSpacingSizes": false, + "spacingScale": { + "default": { + "operator": "*", + "increment": 1.5, + "steps": 7, + "mediumStep": 1.5, + "unit": "rem" + } + }, + "spacingSizes": { + "default": [ + { + "name": "2X-Small", + "slug": "20", + "size": "0.44rem" + }, + { + "name": "X-Small", + "slug": "30", + "size": "0.67rem" + }, + { + "name": "Small", + "slug": "40", + "size": "1rem" + }, + { + "name": "Medium", + "slug": "50", + "size": "1.5rem" + }, + { + "name": "Large", + "slug": "60", + "size": "2.25rem" + }, + { + "name": "X-Large", + "slug": "70", + "size": "3.38rem" + }, + { + "name": "2X-Large", + "slug": "80", + "size": "5.06rem" + } + ], + "theme": [ + { + "name": "1", + "size": "1rem", + "slug": "10" + }, + { + "name": "2", + "size": "min(1.5rem, 2vw)", + "slug": "20" + }, + { + "name": "3", + "size": "min(2.5rem, 3vw)", + "slug": "30" + }, + { + "name": "4", + "size": "min(4rem, 5vw)", + "slug": "40" + }, + { + "name": "5", + "size": "min(6.5rem, 8vw)", + "slug": "50" + }, + { + "name": "6", + "size": "min(10.5rem, 13vw)", + "slug": "60" + } + ] + } + }, + "typography": { + "defaultFontSizes": false, + "dropCap": true, + "fontSizes": { + "default": [ + { + "name": "Small", + "slug": "small", + "size": "13px" + }, + { + "name": "Medium", + "slug": "medium", + "size": "20px" + }, + { + "name": "Large", + "slug": "large", + "size": "36px" + }, + { + "name": "Extra Large", + "slug": "x-large", + "size": "42px" + } + ], + "theme": [ + { + "fluid": false, + "name": "Small", + "size": "0.9rem", + "slug": "small" + }, + { + "fluid": false, + "name": "Medium", + "size": "1.05rem", + "slug": "medium" + }, + { + "fluid": { + "min": "1.39rem", + "max": "1.85rem" + }, + "name": "Large", + "size": "1.85rem", + "slug": "large" + }, + { + "fluid": { + "min": "1.85rem", + "max": "2.5rem" + }, + "name": "Extra Large", + "size": "2.5rem", + "slug": "x-large" + }, + { + "fluid": { + "min": "2.5rem", + "max": "3.27rem" + }, + "name": "Extra Extra Large", + "size": "3.27rem", + "slug": "xx-large" + } + ] + }, + "fontStyle": true, + "fontWeight": true, + "letterSpacing": true, + "textAlign": true, + "textColumns": false, + "textDecoration": true, + "textTransform": true, + "writingMode": true, + "fluid": true, + "fontFamilies": { + "theme": [ + { + "fontFace": [ + { + "fontFamily": "Inter", + "fontStretch": "normal", + "fontStyle": "normal", + "fontWeight": "300 900", + "src": [ + "file:./assets/fonts/inter/Inter-VariableFont_slnt,wght.woff2" + ] + } + ], + "fontFamily": "\"Inter\", sans-serif", + "name": "Inter", + "slug": "body" + }, + { + "fontFace": [ + { + "fontFamily": "Cardo", + "fontStyle": "normal", + "fontWeight": "400", + "src": [ + "file:./assets/fonts/cardo/cardo_normal_400.woff2" + ] + }, + { + "fontFamily": "Cardo", + "fontStyle": "italic", + "fontWeight": "400", + "src": [ + "file:./assets/fonts/cardo/cardo_italic_400.woff2" + ] + }, + { + "fontFamily": "Cardo", + "fontStyle": "normal", + "fontWeight": "700", + "src": [ + "file:./assets/fonts/cardo/cardo_normal_700.woff2" + ] + } + ], + "fontFamily": "Cardo", + "name": "Cardo", + "slug": "heading" + }, + { + "fontFamily": "-apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif", + "name": "System Sans-serif", + "slug": "system-sans-serif" + }, + { + "fontFamily": "Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol", + "name": "System Serif", + "slug": "system-serif" + } + ] + } + }, + "blocks": { + "core/button": { + "border": { + "radius": true + } + }, + "core/image": { + "lightbox": { + "allowEditing": true + } + }, + "core/pullquote": { + "border": { + "color": true, + "radius": true, + "style": true, + "width": true + } + } + }, + "layout": { + "contentSize": "620px", + "wideSize": "1280px" + }, + "background": { + "backgroundImage": true, + "backgroundSize": true + }, + "position": { + "fixed": true, + "sticky": true + } + }, + "colors": [ + { + "color": "#f9f9f9", + "name": "Base", + "slug": "base" + }, + { + "color": "#ffffff", + "name": "Base / Two", + "slug": "base-2" + }, + { + "color": "#111111", + "name": "Contrast", + "slug": "contrast" + }, + { + "color": "#636363", + "name": "Contrast / Two", + "slug": "contrast-2" + }, + { + "color": "#A4A4A4", + "name": "Contrast / Three", + "slug": "contrast-3" + }, + { + "color": "#cfcabe", + "name": "Accent", + "slug": "accent" + }, + { + "color": "#c2a990", + "name": "Accent / Two", + "slug": "accent-2" + }, + { + "color": "#d8613c", + "name": "Accent / Three", + "slug": "accent-3" + }, + { + "color": "#b1c5a4", + "name": "Accent / Four", + "slug": "accent-4" + }, + { + "color": "#b5bdbc", + "name": "Accent / Five", + "slug": "accent-5" + } + ], + "gradients": [ + { + "slug": "gradient-1", + "gradient": "linear-gradient(to bottom, #cfcabe 0%, #F9F9F9 100%)", + "name": "Vertical soft beige to white" + }, + { + "slug": "gradient-2", + "gradient": "linear-gradient(to bottom, #C2A990 0%, #F9F9F9 100%)", + "name": "Vertical soft sandstone to white" + }, + { + "slug": "gradient-3", + "gradient": "linear-gradient(to bottom, #D8613C 0%, #F9F9F9 100%)", + "name": "Vertical soft rust to white" + }, + { + "slug": "gradient-4", + "gradient": "linear-gradient(to bottom, #B1C5A4 0%, #F9F9F9 100%)", + "name": "Vertical soft sage to white" + }, + { + "slug": "gradient-5", + "gradient": "linear-gradient(to bottom, #B5BDBC 0%, #F9F9F9 100%)", + "name": "Vertical soft mint to white" + }, + { + "slug": "gradient-6", + "gradient": "linear-gradient(to bottom, #A4A4A4 0%, #F9F9F9 100%)", + "name": "Vertical soft pewter to white" + }, + { + "slug": "gradient-7", + "gradient": "linear-gradient(to bottom, #cfcabe 50%, #F9F9F9 50%)", + "name": "Vertical hard beige to white" + }, + { + "slug": "gradient-8", + "gradient": "linear-gradient(to bottom, #C2A990 50%, #F9F9F9 50%)", + "name": "Vertical hard sandstone to white" + }, + { + "slug": "gradient-9", + "gradient": "linear-gradient(to bottom, #D8613C 50%, #F9F9F9 50%)", + "name": "Vertical hard rust to white" + }, + { + "slug": "gradient-10", + "gradient": "linear-gradient(to bottom, #B1C5A4 50%, #F9F9F9 50%)", + "name": "Vertical hard sage to white" + }, + { + "slug": "gradient-11", + "gradient": "linear-gradient(to bottom, #B5BDBC 50%, #F9F9F9 50%)", + "name": "Vertical hard mint to white" + }, + { + "slug": "gradient-12", + "gradient": "linear-gradient(to bottom, #A4A4A4 50%, #F9F9F9 50%)", + "name": "Vertical hard pewter to white" + } + ], + "fontSizes": [ + { + "fluid": false, + "name": "Small", + "size": "0.9rem", + "slug": "small" + }, + { + "fluid": false, + "name": "Medium", + "size": "1.05rem", + "slug": "medium" + }, + { + "fluid": { + "min": "1.39rem", + "max": "1.85rem" + }, + "name": "Large", + "size": "1.85rem", + "slug": "large" + }, + { + "fluid": { + "min": "1.85rem", + "max": "2.5rem" + }, + "name": "Extra Large", + "size": "2.5rem", + "slug": "x-large" + }, + { + "fluid": { + "min": "2.5rem", + "max": "3.27rem" + }, + "name": "Extra Extra Large", + "size": "3.27rem", + "slug": "xx-large" + } + ], + "disableCustomSpacingSizes": false, + "spacingSizes": [ + { + "name": "1", + "size": "1rem", + "slug": "10" + }, + { + "name": "2", + "size": "min(1.5rem, 2vw)", + "slug": "20" + }, + { + "name": "3", + "size": "min(2.5rem, 3vw)", + "slug": "30" + }, + { + "name": "4", + "size": "min(4rem, 5vw)", + "slug": "40" + }, + { + "name": "5", + "size": "min(6.5rem, 8vw)", + "slug": "50" + }, + { + "name": "6", + "size": "min(10.5rem, 13vw)", + "slug": "60" + } + ], + "__unstableResolvedAssets": { + "styles": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "scripts": "\n\n\n\n\n\n" + }, + "__unstableIsBlockBasedTheme": true, + "localAutosaveInterval": 15, + "__experimentalDiscussionSettings": { + "commentOrder": "asc", + "commentsPerPage": "50", + "defaultCommentsPage": "newest", + "pageComments": "0", + "threadComments": "1", + "threadCommentsDepth": "5", + "defaultCommentStatus": "open", + "avatarURL": "https://secure.gravatar.com/avatar/?s=96&d=mm&f=y&r=g" + }, + "canUpdateBlockBindings": false +} diff --git a/ios/Tests/GutenbergKitTests/Resources/manifest-test-case-1.json b/ios/Tests/GutenbergKitTests/Resources/manifest-test-case-1.json index 4621a459..22ea46e2 100644 --- a/ios/Tests/GutenbergKitTests/Resources/manifest-test-case-1.json +++ b/ios/Tests/GutenbergKitTests/Resources/manifest-test-case-1.json @@ -1,5 +1,5 @@ { "styles": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n