@swiftbrowser/native 0.0.0-stage → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -2
- package/bin/swiftbrowser-native.mjs +49 -0
- package/dist/bridge.d.ts +50 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +10 -0
- package/dist/bridge.js.map +1 -0
- package/dist/build.d.ts +47 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +156 -0
- package/dist/build.js.map +1 -0
- package/dist/devBuild.d.ts +34 -0
- package/dist/devBuild.d.ts.map +1 -0
- package/dist/devBuild.js +133 -0
- package/dist/devBuild.js.map +1 -0
- package/dist/encoding.d.ts +32 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +136 -0
- package/dist/encoding.js.map +1 -0
- package/dist/fetch.d.ts +117 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +272 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +19 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +17 -0
- package/dist/native.js.map +1 -0
- package/dist/refresh.d.ts +18 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +18 -0
- package/dist/refresh.js.map +1 -0
- package/dist/run.d.ts +27 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +76 -0
- package/dist/run.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +193 -0
- package/dist/runtime.js.map +1 -0
- package/dist/stubs/react-dom-client.d.ts +8 -0
- package/dist/stubs/react-dom-client.d.ts.map +1 -0
- package/dist/stubs/react-dom-client.js +11 -0
- package/dist/stubs/react-dom-client.js.map +1 -0
- package/dist/stubs/react-dom.d.ts +9 -0
- package/dist/stubs/react-dom.d.ts.map +1 -0
- package/dist/stubs/react-dom.js +13 -0
- package/dist/stubs/react-dom.js.map +1 -0
- package/dist/url.d.ts +48 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +213 -0
- package/dist/url.js.map +1 -0
- package/dist/vendor.d.ts +11 -0
- package/dist/vendor.d.ts.map +1 -0
- package/dist/vendor.js +27 -0
- package/dist/vendor.js.map +1 -0
- package/dist/wasmHost.d.ts +35 -0
- package/dist/wasmHost.d.ts.map +1 -0
- package/dist/wasmHost.js +120 -0
- package/dist/wasmHost.js.map +1 -0
- package/package.json +73 -4
- package/src/bridge.ts +58 -0
- package/src/build.ts +185 -0
- package/src/devBuild.ts +152 -0
- package/src/encoding.ts +130 -0
- package/src/fetch.ts +305 -0
- package/src/index.ts +20 -0
- package/src/native.ts +35 -0
- package/src/refresh.ts +29 -0
- package/src/run.ts +98 -0
- package/src/runtime.ts +208 -0
- package/src/stubs/react-dom-client.ts +17 -0
- package/src/stubs/react-dom.ts +15 -0
- package/src/types/react-refresh.d.ts +38 -0
- package/src/url.ts +202 -0
- package/src/vendor.ts +30 -0
- package/src/wasmHost.ts +147 -0
- package/swift/Package.swift +31 -0
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
- package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
- package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
- package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
- package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
- package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
- package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
- package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
- package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +168 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// A JSON value, the shape every op, prop and event crosses the bridge in.
|
|
4
|
+
///
|
|
5
|
+
/// Props are kept as JSON on the tree and read through the typed views in
|
|
6
|
+
/// Props.swift and Style.swift, so an element kind or field this package does
|
|
7
|
+
/// not know yet is carried, not dropped: the protocol grows faster than any
|
|
8
|
+
/// renderer (docs/ops-protocol.md).
|
|
9
|
+
public enum JSONValue: Equatable, Hashable, Sendable {
|
|
10
|
+
case null
|
|
11
|
+
case bool(Bool)
|
|
12
|
+
case number(Double)
|
|
13
|
+
case string(String)
|
|
14
|
+
case array([JSONValue])
|
|
15
|
+
case object([String: JSONValue])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public typealias JSONObject = [String: JSONValue]
|
|
19
|
+
|
|
20
|
+
// MARK: - Reading
|
|
21
|
+
|
|
22
|
+
extension JSONValue {
|
|
23
|
+
public var isNull: Bool {
|
|
24
|
+
if case .null = self { return true }
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public var bool: Bool? {
|
|
29
|
+
if case .bool(let value) = self { return value }
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public var double: Double? {
|
|
34
|
+
if case .number(let value) = self { return value }
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public var int: Int? {
|
|
39
|
+
guard case .number(let value) = self, value.isFinite, value == value.rounded() else { return nil }
|
|
40
|
+
return Int(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public var string: String? {
|
|
44
|
+
if case .string(let value) = self { return value }
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public var array: [JSONValue]? {
|
|
49
|
+
if case .array(let value) = self { return value }
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public var object: JSONObject? {
|
|
54
|
+
if case .object(let value) = self { return value }
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// A member of an object (`.null` when absent or not an object).
|
|
59
|
+
public subscript(key: String) -> JSONValue {
|
|
60
|
+
if case .object(let members) = self, let value = members[key] { return value }
|
|
61
|
+
return .null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// An element of an array (`.null` when out of range or not an array).
|
|
65
|
+
public subscript(index: Int) -> JSONValue {
|
|
66
|
+
if case .array(let elements) = self, elements.indices.contains(index) { return elements[index] }
|
|
67
|
+
return .null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
extension Dictionary where Key == String, Value == JSONValue {
|
|
72
|
+
/// The member as a value with `.null` for a missing key, so chains of
|
|
73
|
+
/// subscripts read like the TypeScript they mirror.
|
|
74
|
+
public func value(_ key: String) -> JSONValue { self[key] ?? .null }
|
|
75
|
+
public func string(_ key: String) -> String? { self[key]?.string }
|
|
76
|
+
public func double(_ key: String) -> Double? { self[key]?.double }
|
|
77
|
+
public func int(_ key: String) -> Int? { self[key]?.int }
|
|
78
|
+
public func bool(_ key: String) -> Bool? { self[key]?.bool }
|
|
79
|
+
public func array(_ key: String) -> [JSONValue]? { self[key]?.array }
|
|
80
|
+
public func object(_ key: String) -> JSONObject? { self[key]?.object }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MARK: - Literals
|
|
84
|
+
|
|
85
|
+
extension JSONValue: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral,
|
|
86
|
+
ExpressibleByBooleanLiteral, ExpressibleByNilLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral
|
|
87
|
+
{
|
|
88
|
+
public init(stringLiteral value: String) { self = .string(value) }
|
|
89
|
+
public init(integerLiteral value: Int) { self = .number(Double(value)) }
|
|
90
|
+
public init(floatLiteral value: Double) { self = .number(value) }
|
|
91
|
+
public init(booleanLiteral value: Bool) { self = .bool(value) }
|
|
92
|
+
public init(nilLiteral: ()) { self = .null }
|
|
93
|
+
public init(arrayLiteral elements: JSONValue...) { self = .array(elements) }
|
|
94
|
+
public init(dictionaryLiteral elements: (String, JSONValue)...) {
|
|
95
|
+
self = .object(Dictionary(elements, uniquingKeysWith: { _, last in last }))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// MARK: - Codable
|
|
100
|
+
|
|
101
|
+
extension JSONValue: Codable {
|
|
102
|
+
public init(from decoder: Decoder) throws {
|
|
103
|
+
let container = try decoder.singleValueContainer()
|
|
104
|
+
if container.decodeNil() {
|
|
105
|
+
self = .null
|
|
106
|
+
} else if let value = try? container.decode(Bool.self) {
|
|
107
|
+
self = .bool(value)
|
|
108
|
+
} else if let value = try? container.decode(Double.self) {
|
|
109
|
+
self = .number(value)
|
|
110
|
+
} else if let value = try? container.decode(String.self) {
|
|
111
|
+
self = .string(value)
|
|
112
|
+
} else if let value = try? container.decode([JSONValue].self) {
|
|
113
|
+
self = .array(value)
|
|
114
|
+
} else if let value = try? container.decode(JSONObject.self) {
|
|
115
|
+
self = .object(value)
|
|
116
|
+
} else {
|
|
117
|
+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "not a JSON value")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public func encode(to encoder: Encoder) throws {
|
|
122
|
+
var container = encoder.singleValueContainer()
|
|
123
|
+
switch self {
|
|
124
|
+
case .null: try container.encodeNil()
|
|
125
|
+
case .bool(let value): try container.encode(value)
|
|
126
|
+
case .number(let value): try container.encode(value)
|
|
127
|
+
case .string(let value): try container.encode(value)
|
|
128
|
+
case .array(let value): try container.encode(value)
|
|
129
|
+
case .object(let value): try container.encode(value)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - Parsing and printing
|
|
135
|
+
|
|
136
|
+
extension JSONValue {
|
|
137
|
+
/// Parses one JSON document.
|
|
138
|
+
public static func parse(_ data: Data) throws -> JSONValue {
|
|
139
|
+
try JSONDecoder().decode(JSONValue.self, from: data)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public static func parse(_ text: String) throws -> JSONValue {
|
|
143
|
+
try parse(Data(text.utf8))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Parses JSON Lines: one document per non-empty line (headless mode's
|
|
147
|
+
/// op stream, the snapshots under Examples/*/__snapshots__).
|
|
148
|
+
public static func parseLines(_ text: String) throws -> [JSONValue] {
|
|
149
|
+
try text.split(whereSeparator: \.isNewline).compactMap { line in
|
|
150
|
+
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
151
|
+
return trimmed.isEmpty ? nil : try parse(trimmed)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The value as compact JSON with object keys sorted, so the same value
|
|
156
|
+
/// always prints the same bytes (tests compare them; the headless runtime
|
|
157
|
+
/// prints sorted keys too).
|
|
158
|
+
public var json: String {
|
|
159
|
+
var out = ""
|
|
160
|
+
write(into: &out)
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func write(into out: inout String) {
|
|
165
|
+
switch self {
|
|
166
|
+
case .null:
|
|
167
|
+
out += "null"
|
|
168
|
+
case .bool(let value):
|
|
169
|
+
out += value ? "true" : "false"
|
|
170
|
+
case .number(let value):
|
|
171
|
+
out += JSONValue.format(value)
|
|
172
|
+
case .string(let value):
|
|
173
|
+
JSONValue.writeString(value, into: &out)
|
|
174
|
+
case .array(let elements):
|
|
175
|
+
out += "["
|
|
176
|
+
for (index, element) in elements.enumerated() {
|
|
177
|
+
if index > 0 { out += "," }
|
|
178
|
+
element.write(into: &out)
|
|
179
|
+
}
|
|
180
|
+
out += "]"
|
|
181
|
+
case .object(let members):
|
|
182
|
+
out += "{"
|
|
183
|
+
for (index, key) in members.keys.sorted().enumerated() {
|
|
184
|
+
if index > 0 { out += "," }
|
|
185
|
+
JSONValue.writeString(key, into: &out)
|
|
186
|
+
out += ":"
|
|
187
|
+
members[key]!.write(into: &out)
|
|
188
|
+
}
|
|
189
|
+
out += "}"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Integral doubles print as integers (`12`, not `12.0`), as JavaScript prints them.
|
|
194
|
+
static func format(_ value: Double) -> String {
|
|
195
|
+
guard value.isFinite else { return "null" }
|
|
196
|
+
if value == value.rounded(), abs(value) < 1e15 { return String(Int64(value)) }
|
|
197
|
+
return String(value)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static func writeString(_ value: String, into out: inout String) {
|
|
201
|
+
out += "\""
|
|
202
|
+
for scalar in value.unicodeScalars {
|
|
203
|
+
switch scalar {
|
|
204
|
+
case "\"": out += "\\\""
|
|
205
|
+
case "\\": out += "\\\\"
|
|
206
|
+
case "\n": out += "\\n"
|
|
207
|
+
case "\r": out += "\\r"
|
|
208
|
+
case "\t": out += "\\t"
|
|
209
|
+
case "\u{08}": out += "\\b"
|
|
210
|
+
case "\u{0C}": out += "\\f"
|
|
211
|
+
default:
|
|
212
|
+
if scalar.value < 0x20 {
|
|
213
|
+
out += String(format: "\\u%04x", scalar.value)
|
|
214
|
+
} else {
|
|
215
|
+
out.unicodeScalars.append(scalar)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
out += "\""
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// MARK: - Bridging to Foundation objects
|
|
224
|
+
|
|
225
|
+
extension JSONValue {
|
|
226
|
+
/// From what `JSONSerialization` (and a JavaScript engine's bridging)
|
|
227
|
+
/// produces: `NSNull`, numbers, strings, arrays and dictionaries.
|
|
228
|
+
public init(any value: Any?) {
|
|
229
|
+
switch value {
|
|
230
|
+
case nil, is NSNull:
|
|
231
|
+
self = .null
|
|
232
|
+
case let value as NSNumber:
|
|
233
|
+
// A Bool bridged to NSNumber keeps the objCType of a char (`c`);
|
|
234
|
+
// every other number reads as a double.
|
|
235
|
+
if String(cString: value.objCType) == "c" || String(cString: value.objCType) == "B" {
|
|
236
|
+
self = .bool(value.boolValue)
|
|
237
|
+
} else {
|
|
238
|
+
self = .number(value.doubleValue)
|
|
239
|
+
}
|
|
240
|
+
case let value as Bool:
|
|
241
|
+
self = .bool(value)
|
|
242
|
+
case let value as Int:
|
|
243
|
+
self = .number(Double(value))
|
|
244
|
+
case let value as Double:
|
|
245
|
+
self = .number(value)
|
|
246
|
+
case let value as String:
|
|
247
|
+
self = .string(value)
|
|
248
|
+
case let value as [Any?]:
|
|
249
|
+
self = .array(value.map { JSONValue(any: $0) })
|
|
250
|
+
case let value as [String: Any?]:
|
|
251
|
+
self = .object(value.mapValues { JSONValue(any: $0) })
|
|
252
|
+
default:
|
|
253
|
+
self = .null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// The value as Foundation objects, for a `JSONSerialization` or an engine bridge.
|
|
258
|
+
public var anyObject: Any {
|
|
259
|
+
switch self {
|
|
260
|
+
case .null: return NSNull()
|
|
261
|
+
case .bool(let value): return value
|
|
262
|
+
case .number(let value): return value
|
|
263
|
+
case .string(let value): return value
|
|
264
|
+
case .array(let elements): return elements.map(\.anyObject)
|
|
265
|
+
case .object(let members): return members.mapValues(\.anyObject)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// One render op (docs/ops-protocol.md, "Ops"). The five that shape the tree
|
|
4
|
+
/// are typed; the rest (`defaults`, `request`, `swiftdata`, `imagedata`, and
|
|
5
|
+
/// headless mode's `event` echo) are carried with their fields.
|
|
6
|
+
public enum Op: Equatable, Sendable {
|
|
7
|
+
/// Create a detached element.
|
|
8
|
+
case create(id: Int, kind: String, props: JSONObject)
|
|
9
|
+
/// Replace the element's props entirely.
|
|
10
|
+
case update(id: Int, props: JSONObject)
|
|
11
|
+
/// Insert element `id` as child `index` of `parent`, moving it if already attached.
|
|
12
|
+
case insert(parent: Int, id: Int, index: Int)
|
|
13
|
+
/// Detach and destroy the element and its subtree.
|
|
14
|
+
case remove(id: Int)
|
|
15
|
+
/// End of a render pass; with `animation`, every change of the pass animates.
|
|
16
|
+
case commit(animation: AnimationSpec?)
|
|
17
|
+
/// Any other op, by name, with the whole object.
|
|
18
|
+
case other(name: String, fields: JSONObject)
|
|
19
|
+
|
|
20
|
+
/// The op's name as the JSON names it.
|
|
21
|
+
public var name: String {
|
|
22
|
+
switch self {
|
|
23
|
+
case .create: return "create"
|
|
24
|
+
case .update: return "update"
|
|
25
|
+
case .insert: return "insert"
|
|
26
|
+
case .remove: return "remove"
|
|
27
|
+
case .commit: return "commit"
|
|
28
|
+
case .other(let name, _): return name
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public struct OpDecodingError: Error, CustomStringConvertible, Equatable {
|
|
34
|
+
public let message: String
|
|
35
|
+
public init(_ message: String) { self.message = message }
|
|
36
|
+
public var description: String { message }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
extension Op {
|
|
40
|
+
/// Decodes one op from its JSON object.
|
|
41
|
+
public init(json: JSONValue) throws {
|
|
42
|
+
guard let fields = json.object else { throw OpDecodingError("an op is a JSON object, got \(json.json)") }
|
|
43
|
+
guard let name = fields.string("op") else { throw OpDecodingError("an op needs an \"op\" field: \(json.json)") }
|
|
44
|
+
func id(_ key: String) throws -> Int {
|
|
45
|
+
guard let value = fields.int(key) else { throw OpDecodingError("\(name) needs an integer \"\(key)\": \(json.json)") }
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
switch name {
|
|
49
|
+
case "create":
|
|
50
|
+
guard let kind = fields.string("kind") else { throw OpDecodingError("create needs a \"kind\": \(json.json)") }
|
|
51
|
+
self = .create(id: try id("id"), kind: kind, props: fields.object("props") ?? [:])
|
|
52
|
+
case "update":
|
|
53
|
+
self = .update(id: try id("id"), props: fields.object("props") ?? [:])
|
|
54
|
+
case "insert":
|
|
55
|
+
self = .insert(parent: try id("parent"), id: try id("id"), index: try id("index"))
|
|
56
|
+
case "remove":
|
|
57
|
+
self = .remove(id: try id("id"))
|
|
58
|
+
case "commit":
|
|
59
|
+
self = .commit(animation: fields["animation"].flatMap(AnimationSpec.init(json:)))
|
|
60
|
+
default:
|
|
61
|
+
self = .other(name: name, fields: fields)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Decodes a JSON array of ops (what a render pass crosses the bridge as).
|
|
66
|
+
public static func decodeArray(_ data: Data) throws -> [Op] {
|
|
67
|
+
let value = try JSONValue.parse(data)
|
|
68
|
+
guard let elements = value.array else { throw OpDecodingError("a batch of ops is a JSON array") }
|
|
69
|
+
return try elements.map(Op.init(json:))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static func decodeArray(_ text: String) throws -> [Op] {
|
|
73
|
+
try decodeArray(Data(text.utf8))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Decodes JSON Lines, one op per line (headless mode, the snapshots).
|
|
77
|
+
public static func decodeLines(_ text: String) throws -> [Op] {
|
|
78
|
+
try JSONValue.parseLines(text).map(Op.init(json:))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The op as JSON (sorted keys), for logs and tests.
|
|
82
|
+
public var json: JSONValue {
|
|
83
|
+
switch self {
|
|
84
|
+
case .create(let id, let kind, let props):
|
|
85
|
+
return .object(["op": "create", "id": .number(Double(id)), "kind": .string(kind), "props": .object(props)])
|
|
86
|
+
case .update(let id, let props):
|
|
87
|
+
return .object(["op": "update", "id": .number(Double(id)), "props": .object(props)])
|
|
88
|
+
case .insert(let parent, let id, let index):
|
|
89
|
+
return .object(["op": "insert", "parent": .number(Double(parent)), "id": .number(Double(id)), "index": .number(Double(index))])
|
|
90
|
+
case .remove(let id):
|
|
91
|
+
return .object(["op": "remove", "id": .number(Double(id))])
|
|
92
|
+
case .commit(let animation):
|
|
93
|
+
var fields: JSONObject = ["op": "commit"]
|
|
94
|
+
if let animation { fields["animation"] = animation.json }
|
|
95
|
+
return .object(fields)
|
|
96
|
+
case .other(_, let fields):
|
|
97
|
+
return .object(fields)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|