@l22-io/orchard-mcp 0.3.1 → 0.6.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.
Files changed (42) hide show
  1. package/README.md +11 -8
  2. package/build/bridge.js +18 -0
  3. package/build/bridge.js.map +1 -1
  4. package/build/index.js +11 -1
  5. package/build/index.js.map +1 -1
  6. package/build/tools/contacts.d.ts +2 -0
  7. package/build/tools/contacts.js +37 -0
  8. package/build/tools/contacts.js.map +1 -0
  9. package/build/tools/files.js +4 -1
  10. package/build/tools/files.js.map +1 -1
  11. package/build/tools/keynote.d.ts +2 -0
  12. package/build/tools/keynote.js +198 -0
  13. package/build/tools/keynote.js.map +1 -0
  14. package/build/tools/mail.js +57 -10
  15. package/build/tools/mail.js.map +1 -1
  16. package/build/tools/notes.d.ts +2 -0
  17. package/build/tools/notes.js +83 -0
  18. package/build/tools/notes.js.map +1 -0
  19. package/build/tools/numbers.d.ts +2 -0
  20. package/build/tools/numbers.js +167 -0
  21. package/build/tools/numbers.js.map +1 -0
  22. package/build/tools/pages.d.ts +2 -0
  23. package/build/tools/pages.js +143 -0
  24. package/build/tools/pages.js.map +1 -0
  25. package/package.json +14 -9
  26. package/scripts/postinstall.sh +77 -8
  27. package/swift/.build/AppleBridge.app/Contents/MacOS/apple-bridge +0 -0
  28. package/swift/.build/AppleBridge.app.sha256 +1 -0
  29. package/swift/Package.resolved +14 -0
  30. package/swift/Package.swift +28 -0
  31. package/swift/Sources/AppleBridge/AppleBridge.swift +846 -0
  32. package/swift/Sources/AppleBridge/Calendar.swift +221 -0
  33. package/swift/Sources/AppleBridge/Contacts.swift +225 -0
  34. package/swift/Sources/AppleBridge/Doctor.swift +252 -0
  35. package/swift/Sources/AppleBridge/Files.swift +474 -0
  36. package/swift/Sources/AppleBridge/JSON.swift +57 -0
  37. package/swift/Sources/AppleBridge/Keynote.swift +599 -0
  38. package/swift/Sources/AppleBridge/Mail.swift +794 -0
  39. package/swift/Sources/AppleBridge/Notes.swift +263 -0
  40. package/swift/Sources/AppleBridge/Numbers.swift +601 -0
  41. package/swift/Sources/AppleBridge/Pages.swift +467 -0
  42. package/swift/Sources/AppleBridge/Reminders.swift +347 -0
@@ -0,0 +1,599 @@
1
+ import Foundation
2
+
3
+ enum KeynoteBridge {
4
+
5
+ // MARK: - Info
6
+
7
+ static func info(file: String) {
8
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
9
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
10
+ return
11
+ }
12
+ let escaped = escapeForAppleScript(resolvedFile)
13
+ let script = """
14
+ tell application "Keynote"
15
+ set doc to open POSIX file "\(escaped)"
16
+ set docName to name of doc
17
+ set sc to count of slides of doc
18
+ set themeName to name of document theme of doc
19
+ close doc saving no
20
+ return docName & "|||" & (sc as string) & "|||" & themeName
21
+ end tell
22
+ """
23
+
24
+ guard let raw = runAppleScript(script) else { return }
25
+ let parts = raw.components(separatedBy: "|||")
26
+ guard parts.count >= 3 else {
27
+ JSONOutput.error("Unexpected response format from Keynote")
28
+ return
29
+ }
30
+
31
+ let result: [String: Any] = [
32
+ "name": parts[0].trimmingCharacters(in: .whitespaces),
33
+ "slideCount": Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 0,
34
+ "theme": parts[2].trimmingCharacters(in: .whitespaces),
35
+ "path": resolvedFile
36
+ ]
37
+ JSONOutput.success(result)
38
+ }
39
+
40
+ // MARK: - Read
41
+
42
+ static func read(file: String, slideIndex: Int?) {
43
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
44
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
45
+ return
46
+ }
47
+ let escaped = escapeForAppleScript(resolvedFile)
48
+
49
+ let slideScript: String
50
+ if let idx = slideIndex {
51
+ slideScript = """
52
+ set slideList to {slide \(idx) of doc}
53
+ """
54
+ } else {
55
+ slideScript = """
56
+ set slideList to every slide of doc
57
+ """
58
+ }
59
+
60
+ let script = """
61
+ tell application "Keynote"
62
+ set doc to open POSIX file "\(escaped)"
63
+ \(slideScript)
64
+ set resultParts to {}
65
+ set slideIdx to 0
66
+ repeat with s in slideList
67
+ set slideIdx to slideIdx + 1
68
+ set actualIdx to slideIdx
69
+ \(slideIndex != nil ? "set actualIdx to \(slideIndex!)" : "")
70
+ set slideTitle to ""
71
+ try
72
+ set slideTitle to object text of default title item of s
73
+ end try
74
+ set slideBody to ""
75
+ try
76
+ set slideBody to object text of default body item of s
77
+ end try
78
+ set slideNotes to presenter notes of s
79
+ set slideLayout to name of base slide of s
80
+ set slideSkipped to skipped of s
81
+ set slideLine to (actualIdx as string) & "|||" & slideTitle & "|||" & slideBody & "|||" & slideNotes & "|||" & slideLayout & "|||" & (slideSkipped as string)
82
+ set end of resultParts to slideLine
83
+ end repeat
84
+ close doc saving no
85
+ set oldDelim to AppleScript's text item delimiters
86
+ set AppleScript's text item delimiters to "###"
87
+ set resultText to resultParts as string
88
+ set AppleScript's text item delimiters to oldDelim
89
+ return resultText
90
+ end tell
91
+ """
92
+
93
+ guard let raw = runAppleScript(script) else { return }
94
+ let slides = parseSlideList(raw)
95
+ JSONOutput.success(slides)
96
+ }
97
+
98
+ // MARK: - Create
99
+
100
+ static func create(file: String, theme: String?) {
101
+ guard let resolvedFile = FilesBridge.validatePath(file, mustExist: false) else {
102
+ JSONOutput.error("Path is outside home directory: \(file)")
103
+ return
104
+ }
105
+ let escaped = escapeForAppleScript(resolvedFile)
106
+
107
+ let themeClause: String
108
+ if let theme = theme {
109
+ themeClause = "set doc to make new document with properties {document theme:theme \"\(escapeForAppleScript(theme))\"}"
110
+ } else {
111
+ themeClause = "set doc to make new document"
112
+ }
113
+
114
+ let script = """
115
+ tell application "Keynote"
116
+ \(themeClause)
117
+ set docPath to POSIX file "\(escaped)"
118
+ save doc in docPath
119
+ close doc
120
+ return "ok"
121
+ end tell
122
+ """
123
+
124
+ guard let _ = runAppleScript(script) else { return }
125
+ JSONOutput.success(["path": resolvedFile, "created": true])
126
+ }
127
+
128
+ // MARK: - Add Slide
129
+
130
+ static func addSlide(file: String, layout: String?, title: String?, body: String?, notes: String?, position: Int?) {
131
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
132
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
133
+ return
134
+ }
135
+ let escaped = escapeForAppleScript(resolvedFile)
136
+
137
+ let makeClause: String
138
+ if let pos = position {
139
+ makeClause = "set newSlide to make new slide at after slide \(pos) of doc"
140
+ } else {
141
+ makeClause = "set newSlide to make new slide at end of doc"
142
+ }
143
+
144
+ let layoutClause: String
145
+ if let layout = layout {
146
+ layoutClause = """
147
+ try
148
+ set base slide of newSlide to base slide "\(escapeForAppleScript(layout))" of document theme of doc
149
+ end try
150
+ """
151
+ } else {
152
+ layoutClause = ""
153
+ }
154
+
155
+ let titleClause: String
156
+ if let title = title {
157
+ titleClause = """
158
+ try
159
+ set object text of default title item of newSlide to "\(escapeForAppleScript(title))"
160
+ end try
161
+ """
162
+ } else {
163
+ titleClause = ""
164
+ }
165
+
166
+ let bodyClause: String
167
+ if let body = body {
168
+ bodyClause = """
169
+ try
170
+ set object text of default body item of newSlide to "\(escapeForAppleScript(body))"
171
+ end try
172
+ """
173
+ } else {
174
+ bodyClause = ""
175
+ }
176
+
177
+ let notesClause: String
178
+ if let notes = notes {
179
+ notesClause = """
180
+ set presenter notes of newSlide to "\(escapeForAppleScript(notes))"
181
+ """
182
+ } else {
183
+ notesClause = ""
184
+ }
185
+
186
+ let script = """
187
+ tell application "Keynote"
188
+ set doc to open POSIX file "\(escaped)"
189
+ \(makeClause)
190
+ \(layoutClause)
191
+ \(titleClause)
192
+ \(bodyClause)
193
+ \(notesClause)
194
+ set sc to count of slides of doc
195
+ save doc
196
+ close doc
197
+ return sc as string
198
+ end tell
199
+ """
200
+
201
+ guard let raw = runAppleScript(script) else { return }
202
+ let slideCount = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
203
+ JSONOutput.success(["path": resolvedFile, "slideCount": slideCount])
204
+ }
205
+
206
+ // MARK: - Edit Slide
207
+
208
+ static func editSlide(file: String, slideIndex: Int, title: String?, body: String?, notes: String?) {
209
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
210
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
211
+ return
212
+ }
213
+ let escaped = escapeForAppleScript(resolvedFile)
214
+
215
+ let titleClause: String
216
+ if let title = title {
217
+ titleClause = """
218
+ try
219
+ set object text of default title item of s to "\(escapeForAppleScript(title))"
220
+ end try
221
+ """
222
+ } else {
223
+ titleClause = ""
224
+ }
225
+
226
+ let bodyClause: String
227
+ if let body = body {
228
+ bodyClause = """
229
+ try
230
+ set object text of default body item of s to "\(escapeForAppleScript(body))"
231
+ end try
232
+ """
233
+ } else {
234
+ bodyClause = ""
235
+ }
236
+
237
+ let notesClause: String
238
+ if let notes = notes {
239
+ notesClause = """
240
+ set presenter notes of s to "\(escapeForAppleScript(notes))"
241
+ """
242
+ } else {
243
+ notesClause = ""
244
+ }
245
+
246
+ let script = """
247
+ tell application "Keynote"
248
+ set doc to open POSIX file "\(escaped)"
249
+ set s to slide \(slideIndex) of doc
250
+ \(titleClause)
251
+ \(bodyClause)
252
+ \(notesClause)
253
+ save doc
254
+ close doc
255
+ return "ok"
256
+ end tell
257
+ """
258
+
259
+ guard let _ = runAppleScript(script) else { return }
260
+ JSONOutput.success(["path": resolvedFile, "slide": slideIndex, "edited": true])
261
+ }
262
+
263
+ // MARK: - Remove Slide
264
+
265
+ static func removeSlide(file: String, slideIndex: Int) {
266
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
267
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
268
+ return
269
+ }
270
+ let escaped = escapeForAppleScript(resolvedFile)
271
+ let script = """
272
+ tell application "Keynote"
273
+ set doc to open POSIX file "\(escaped)"
274
+ delete slide \(slideIndex) of doc
275
+ set sc to count of slides of doc
276
+ save doc
277
+ close doc
278
+ return sc as string
279
+ end tell
280
+ """
281
+
282
+ guard let raw = runAppleScript(script) else { return }
283
+ let remaining = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
284
+ JSONOutput.success(["path": resolvedFile, "remainingSlides": remaining])
285
+ }
286
+
287
+ // MARK: - Reorder Slides
288
+
289
+ static func reorderSlides(file: String, from: Int, to: Int) {
290
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
291
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
292
+ return
293
+ }
294
+ let escaped = escapeForAppleScript(resolvedFile)
295
+
296
+ let moveClause: String
297
+ if to < from {
298
+ moveClause = "move slide \(from) of doc to before slide \(to) of doc"
299
+ } else {
300
+ moveClause = "move slide \(from) of doc to after slide \(to) of doc"
301
+ }
302
+
303
+ let script = """
304
+ tell application "Keynote"
305
+ set doc to open POSIX file "\(escaped)"
306
+ \(moveClause)
307
+ save doc
308
+ close doc
309
+ return "ok"
310
+ end tell
311
+ """
312
+
313
+ guard let _ = runAppleScript(script) else { return }
314
+ JSONOutput.success(["path": resolvedFile, "movedFrom": from, "movedTo": to])
315
+ }
316
+
317
+ // MARK: - List Themes
318
+
319
+ static func listThemes() {
320
+ let script = """
321
+ tell application "Keynote"
322
+ set themeNames to name of every theme
323
+ set oldDelim to AppleScript's text item delimiters
324
+ set AppleScript's text item delimiters to "|||"
325
+ set resultText to themeNames as string
326
+ set AppleScript's text item delimiters to oldDelim
327
+ return resultText
328
+ end tell
329
+ """
330
+
331
+ guard let raw = runAppleScript(script) else { return }
332
+ let themes = raw.components(separatedBy: "|||")
333
+ .map { $0.trimmingCharacters(in: .whitespaces) }
334
+ .filter { !$0.isEmpty }
335
+ JSONOutput.success(themes)
336
+ }
337
+
338
+ // MARK: - Export
339
+
340
+ static func export(file: String, format: String, dest: String?, slideIndex: Int?) {
341
+ guard let resolvedFile = FilesBridge.validatePath(file) else {
342
+ JSONOutput.error("Path is outside home directory or does not exist: \(file)")
343
+ return
344
+ }
345
+ let escapedFile = escapeForAppleScript(resolvedFile)
346
+
347
+ let isImageExport = (format == "png" || format == "jpeg")
348
+
349
+ if isImageExport && slideIndex == nil {
350
+ exportSlideImages(file: resolvedFile, format: format, dest: dest)
351
+ return
352
+ }
353
+
354
+ let outputPath: String
355
+ if let dest = dest {
356
+ guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
357
+ JSONOutput.error("Destination path is outside home directory: \(dest)")
358
+ return
359
+ }
360
+ outputPath = resolvedDest
361
+ } else {
362
+ let ext: String
363
+ switch format {
364
+ case "pdf": ext = "pdf"
365
+ case "pptx": ext = "pptx"
366
+ case "png": ext = "png"
367
+ case "jpeg": ext = "jpeg"
368
+ default: ext = format
369
+ }
370
+ outputPath = resolvedFile.replacingOccurrences(of: ".key", with: ".\(ext)")
371
+ }
372
+ let escapedOutput = escapeForAppleScript(outputPath)
373
+
374
+ let formatMap: [String: String] = [
375
+ "pdf": "PDF",
376
+ "pptx": "Microsoft PowerPoint",
377
+ "png": "PNG",
378
+ "jpeg": "JPEG"
379
+ ]
380
+ guard let keynoteFormat = formatMap[format] else {
381
+ JSONOutput.error("Unsupported export format: \(format). Use pdf, pptx, png, or jpeg.")
382
+ return
383
+ }
384
+
385
+ if isImageExport, let idx = slideIndex {
386
+ let script = """
387
+ tell application "Keynote"
388
+ set doc to open POSIX file "\(escapedFile)"
389
+ export doc as slide images to POSIX file "\(escapedOutput)" with properties {image format:\(keynoteFormat), skipped slides:false}
390
+ close doc saving no
391
+ return "ok"
392
+ end tell
393
+ """
394
+ guard let _ = runAppleScript(script) else { return }
395
+
396
+ let fm = FileManager.default
397
+ let dirURL = URL(fileURLWithPath: outputPath).deletingLastPathComponent()
398
+ let ext = format == "jpeg" ? "jpeg" : "png"
399
+ let pattern = dirURL.path
400
+ if let files = try? fm.contentsOfDirectory(atPath: pattern) {
401
+ let sorted = files.filter { $0.hasSuffix(ext) }.sorted()
402
+ if idx > 0 && idx <= sorted.count {
403
+ let targetFile = dirURL.appendingPathComponent(sorted[idx - 1]).path
404
+ JSONOutput.success(["path": targetFile])
405
+ return
406
+ }
407
+ }
408
+ JSONOutput.success(["path": outputPath])
409
+ return
410
+ }
411
+
412
+ let script = """
413
+ tell application "Keynote"
414
+ set doc to open POSIX file "\(escapedFile)"
415
+ export doc to POSIX file "\(escapedOutput)" as \(keynoteFormat)
416
+ close doc saving no
417
+ return "ok"
418
+ end tell
419
+ """
420
+
421
+ guard let _ = runAppleScript(script) else { return }
422
+ JSONOutput.success(["path": outputPath])
423
+ }
424
+
425
+ private static func exportSlideImages(file: String, format: String, dest: String?) {
426
+ let escapedFile = escapeForAppleScript(file)
427
+ let outputDir: String
428
+ if let dest = dest {
429
+ guard let resolvedDest = FilesBridge.validatePath(dest, mustExist: false) else {
430
+ JSONOutput.error("Destination path is outside home directory: \(dest)")
431
+ return
432
+ }
433
+ outputDir = resolvedDest
434
+ } else {
435
+ let base = file.replacingOccurrences(of: ".key", with: "_slides")
436
+ outputDir = base
437
+ }
438
+ let escapedOutput = escapeForAppleScript(outputDir)
439
+
440
+ let imageFormat = format == "jpeg" ? "JPEG" : "PNG"
441
+
442
+ let fm = FileManager.default
443
+ if !fm.fileExists(atPath: outputDir) {
444
+ do {
445
+ try fm.createDirectory(atPath: outputDir, withIntermediateDirectories: true)
446
+ } catch {
447
+ JSONOutput.error("Failed to create output directory: \(error.localizedDescription)")
448
+ return
449
+ }
450
+ }
451
+
452
+ let script = """
453
+ tell application "Keynote"
454
+ set doc to open POSIX file "\(escapedFile)"
455
+ export doc as slide images to POSIX file "\(escapedOutput)" with properties {image format:\(imageFormat)}
456
+ close doc saving no
457
+ return "ok"
458
+ end tell
459
+ """
460
+
461
+ guard let _ = runAppleScript(script) else { return }
462
+
463
+ let ext = format == "jpeg" ? "jpeg" : "png"
464
+ var exportedFiles: [String] = []
465
+ if let files = try? fm.contentsOfDirectory(atPath: outputDir) {
466
+ exportedFiles = files.filter { $0.hasSuffix(ext) || $0.hasSuffix("jpg") }
467
+ .sorted()
468
+ .map { "\(outputDir)/\($0)" }
469
+ }
470
+
471
+ JSONOutput.success([
472
+ "directory": outputDir,
473
+ "files": exportedFiles,
474
+ "count": exportedFiles.count
475
+ ] as [String: Any])
476
+ }
477
+
478
+ // MARK: - Search
479
+
480
+ static func search(query: String, limit: Int) {
481
+ let sanitized = query.replacingOccurrences(of: "'", with: "")
482
+ .replacingOccurrences(of: "\\", with: "")
483
+ let task = Process()
484
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/mdfind")
485
+ task.arguments = [
486
+ "kMDItemContentType == 'com.apple.iWork.keynote.sffkey' && kMDItemDisplayName == '*\(sanitized)*'cd"
487
+ ]
488
+
489
+ let pipe = Pipe()
490
+ task.standardOutput = pipe
491
+ task.standardError = Pipe()
492
+
493
+ do {
494
+ try task.run()
495
+ task.waitUntilExit()
496
+
497
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
498
+ guard let output = String(data: data, encoding: .utf8) else {
499
+ JSONOutput.success([])
500
+ return
501
+ }
502
+
503
+ let paths = output.components(separatedBy: "\n")
504
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
505
+ .filter { !$0.isEmpty }
506
+ .prefix(limit)
507
+
508
+ let fm = FileManager.default
509
+ let results: [[String: Any]] = paths.compactMap { path in
510
+ guard let attrs = try? fm.attributesOfItem(atPath: path) else { return nil }
511
+ var entry: [String: Any] = [
512
+ "path": path,
513
+ "name": URL(fileURLWithPath: path).lastPathComponent
514
+ ]
515
+ if let size = attrs[.size] as? Int {
516
+ entry["size"] = size
517
+ }
518
+ if let modified = attrs[.modificationDate] as? Date {
519
+ entry["modified"] = iso8601(modified)
520
+ }
521
+ return entry
522
+ }
523
+
524
+ JSONOutput.success(results)
525
+ } catch {
526
+ JSONOutput.error("Spotlight search failed: \(error.localizedDescription)")
527
+ }
528
+ }
529
+
530
+ // MARK: - AppleScript Execution
531
+
532
+ private static func runAppleScript(_ script: String) -> String? {
533
+ let task = Process()
534
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
535
+ task.arguments = ["-e", script]
536
+
537
+ let outPipe = Pipe()
538
+ let errPipe = Pipe()
539
+ task.standardOutput = outPipe
540
+ task.standardError = errPipe
541
+
542
+ do {
543
+ try task.run()
544
+ task.waitUntilExit()
545
+
546
+ if task.terminationStatus != 0 {
547
+ let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
548
+ let errStr = String(data: errData, encoding: .utf8)?
549
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
550
+
551
+ if errStr.contains("-1743") || errStr.contains("not allowed") {
552
+ JSONOutput.error("Keynote automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
553
+ } else if errStr.contains("-600") || errStr.contains("not running") {
554
+ JSONOutput.error("Keynote is not running. It will be launched automatically on next attempt.")
555
+ } else {
556
+ JSONOutput.error("AppleScript error: \(errStr)")
557
+ }
558
+ return nil
559
+ }
560
+
561
+ let data = outPipe.fileHandleForReading.readDataToEndOfFile()
562
+ return String(data: data, encoding: .utf8)?
563
+ .trimmingCharacters(in: .whitespacesAndNewlines)
564
+ } catch {
565
+ JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
566
+ return nil
567
+ }
568
+ }
569
+
570
+ private static func escapeForAppleScript(_ str: String) -> String {
571
+ return str.replacingOccurrences(of: "\\", with: "\\\\")
572
+ .replacingOccurrences(of: "\"", with: "\\\"")
573
+ .replacingOccurrences(of: "\n", with: "\\n")
574
+ .replacingOccurrences(of: "\r", with: "\\r")
575
+ .replacingOccurrences(of: "\t", with: "\\t")
576
+ }
577
+
578
+ // MARK: - Parsers
579
+
580
+ private static func parseSlideList(_ raw: String) -> [[String: Any]] {
581
+ guard !raw.isEmpty else { return [] }
582
+ let slideChunks = raw.components(separatedBy: "###")
583
+ return slideChunks.compactMap { chunk -> [String: Any]? in
584
+ let trimmed = chunk.trimmingCharacters(in: .whitespacesAndNewlines)
585
+ guard !trimmed.isEmpty else { return nil }
586
+ let parts = trimmed.components(separatedBy: "|||")
587
+ guard parts.count >= 6 else { return nil }
588
+
589
+ return [
590
+ "index": Int(parts[0].trimmingCharacters(in: .whitespaces)) ?? 0,
591
+ "title": parts[1].trimmingCharacters(in: .whitespaces),
592
+ "body": parts[2].trimmingCharacters(in: .whitespaces),
593
+ "notes": parts[3].trimmingCharacters(in: .whitespaces),
594
+ "layout": parts[4].trimmingCharacters(in: .whitespaces),
595
+ "skipped": parts[5].trimmingCharacters(in: .whitespaces).lowercased() == "true"
596
+ ]
597
+ }
598
+ }
599
+ }