Sources/OrgApp/TangleWriter.swift
71 lines · 2981 bytes
1import Foundation
2import OrgCore
3
4/// Writes what `Tangle.run` produced, as `org-babel-tangle` does: a file whose contents
5/// haven't changed is left alone, `:mkdirp` makes its folder, a read-only file is replaced,
6/// and the mode is set after writing.
7public enum TangleWriter {
8 public struct Failure: Error, Equatable {
9 public let message: String
10 }
11
12 /// The paths written or already up to date, in order.
13 @discardableResult
14 public static func write(_ output: Tangle.Output, source: String) throws(Failure) -> [String] {
15 let manager = FileManager.default
16 var paths: [String] = []
17 for file in output.files {
18 let url = URL(fileURLWithPath: file.path)
19 let data = Data(file.contents.utf8)
20 if let existing = manager.contents(atPath: file.path), existing == data {
21 paths.append(file.path)
22 continue
23 }
24 if (file.path as NSString).standardizingPath == (source as NSString).standardizingPath {
25 throw Failure(message: "Not allowed to tangle into the same file as self")
26 }
27 let folder = url.deletingLastPathComponent()
28 if file.makeDirectories {
29 do {
30 try manager.createDirectory(at: folder, withIntermediateDirectories: true)
31 } catch {
32 throw Failure(message: "Can't make \(folder.path): \(error.localizedDescription)")
33 }
34 }
35 if manager.fileExists(atPath: file.path), !manager.isWritableFile(atPath: file.path) {
36 try? manager.removeItem(at: url)
37 }
38 do {
39 try data.write(to: url)
40 if let mode = file.mode { try manager.setAttributes([.posixPermissions: mode], ofItemAtPath: file.path) }
41 } catch {
42 throw Failure(message: "Can't write \(file.path): \(error.localizedDescription)")
43 }
44 paths.append(file.path)
45 }
46 return paths
47 }
48
49 /// `org-babel-tangle`'s message.
50 public static func message(_ output: Tangle.Output, source: String) -> String {
51 "Tangled \(output.blocks) code block\(output.blocks == 1 ? "" : "s") from \((source as NSString).lastPathComponent)"
52 }
53}
54
55extension DocumentSession {
56 /// Tangles the open file: its text as it is now, written beside it.
57 public func tangle(_ scope: Tangle.Scope) -> String {
58 guard let path, let document else { return "No file is open" }
59 switch Tangle.run(text: document.text, path: path, scope: scope, tree: document.tree) {
60 case .failure(.message(let message)):
61 return message
62 case .success(let output):
63 do {
64 try TangleWriter.write(output, source: path)
65 return TangleWriter.message(output, source: path)
66 } catch {
67 return error.message
68 }
69 }
70 }
71}