krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgApp/TangleWriter.swift

16086b4cf2caff5328774b2cd5ae3ffe1ab65ca4
orgstar/Sources/OrgApp/TangleWriter.swift history · blame · raw

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}