CutBox.app

Coverage Report

Created: 2024-03-12 03:40

.../Source/App/Services/JSFuncService.swift
Line
Count
Source (jump to first uncovered line)
1
//
2
//  JSFuncService.swift
3
//  CutBox
4
//
5
//  Created by Jason Milkins on 17/5/18.
6
//  Copyright © 2018-2023 ocodo. All rights reserved.
7
//
8
9
import JavaScriptCore
10
11
class JSFuncService: NSObject {
12
    static var context: JSContext = JSContext()
13
14
    var prefs: CutBoxPreferencesService?
15
16
396
    let require: @convention(block) (String) -> JSValue? = { path in
17
396
        let expandedPath = NSString(string: path).expandingTildeInPath
18
396
19
396
        guard FileManager.default.fileExists(atPath: expandedPath)
20
396
            else {
21
1
                notifyUser(
22
1
                    title: "Require: filename \(expandedPath)",
23
1
                    info: " does not exist")
24
1
                return nil
25
3
        }
26
3
27
3
        guard let fileContent = getStringFromFile(expandedPath)
28
3
            else {
29
1
                notifyUser(
30
1
                    title: "Cannot read: \(expandedPath)",
31
1
                    info: "unknown error processing file")
32
1
                return nil
33
2
        }
34
2
35
2
        return JSFuncService.context.evaluateScript(fileContent)
36
3
    }
$s15CutBoxUnitTests13JSFuncServiceC7requireySo7JSValueCSgSSXBvpfi
Line
Count
Source
16
392
    let require: @convention(block) (String) -> JSValue? = { path in
17
392
        let expandedPath = NSString(string: path).expandingTildeInPath
18
392
19
392
        guard FileManager.default.fileExists(atPath: expandedPath)
20
392
            else {
21
392
                notifyUser(
22
392
                    title: "Require: filename \(expandedPath)",
23
392
                    info: " does not exist")
24
392
                return nil
25
392
        }
26
392
27
392
        guard let fileContent = getStringFromFile(expandedPath)
28
392
            else {
29
392
                notifyUser(
30
392
                    title: "Cannot read: \(expandedPath)",
31
392
                    info: "unknown error processing file")
32
392
                return nil
33
392
        }
34
392
35
392
        return JSFuncService.context.evaluateScript(fileContent)
36
392
    }
$s15CutBoxUnitTests13JSFuncServiceC7requireySo7JSValueCSgSSXBvpfiAGSScfU_
Line
Count
Source
16
4
    let require: @convention(block) (String) -> JSValue? = { path in
17
4
        let expandedPath = NSString(string: path).expandingTildeInPath
18
4
19
4
        guard FileManager.default.fileExists(atPath: expandedPath)
20
4
            else {
21
1
                notifyUser(
22
1
                    title: "Require: filename \(expandedPath)",
23
1
                    info: " does not exist")
24
1
                return nil
25
3
        }
26
3
27
3
        guard let fileContent = getStringFromFile(expandedPath)
28
3
            else {
29
1
                notifyUser(
30
1
                    title: "Cannot read: \(expandedPath)",
31
1
                    info: "unknown error processing file")
32
1
                return nil
33
2
        }
34
2
35
2
        return JSFuncService.context.evaluateScript(fileContent)
36
3
    }
37
38
393
    let shellCommand: @convention(block) (String) -> String = { command in
39
393
        let task = Process()
40
393
        task.launchPath = "/bin/bash"
41
393
        task.arguments = ["-c", command]
42
393
43
393
        let pipe = Pipe()
44
393
        task.standardOutput = pipe
45
393
        task.launch()
46
393
47
393
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
48
393
        return String(data: data, encoding: .utf8) ?? ""
49
393
    }
$s15CutBoxUnitTests13JSFuncServiceC12shellCommandyS2SXBvpfi
Line
Count
Source
38
392
    let shellCommand: @convention(block) (String) -> String = { command in
39
392
        let task = Process()
40
392
        task.launchPath = "/bin/bash"
41
392
        task.arguments = ["-c", command]
42
392
43
392
        let pipe = Pipe()
44
392
        task.standardOutput = pipe
45
392
        task.launch()
46
392
47
392
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
48
392
        return String(data: data, encoding: .utf8) ?? ""
49
392
    }
$s15CutBoxUnitTests13JSFuncServiceC12shellCommandyS2SXBvpfiS2ScfU_
Line
Count
Source
38
1
    let shellCommand: @convention(block) (String) -> String = { command in
39
1
        let task = Process()
40
1
        task.launchPath = "/bin/bash"
41
1
        task.arguments = ["-c", command]
42
1
43
1
        let pipe = Pipe()
44
1
        task.standardOutput = pipe
45
1
        task.launch()
46
1
47
1
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
48
1
        return String(data: data, encoding: .utf8) ?? ""
49
1
    }
50
51
    let shell: @convention(block)
52
394
    (String, String?) -> [String] = { command, stdin -> [String] in
53
394
        let task = Process()
54
394
55
394
        if let stdinString = stdin {
56
2
            let stdinPipe = Pipe()
57
2
            task.standardInput = stdinPipe
58
2
            if let stdinData = stdinString.data(using: .utf8) {
59
2
                stdinPipe.fileHandleForWriting.write(stdinData)
60
2
                stdinPipe.fileHandleForWriting.closeFile()
61
2
            }
62
2
        }
63
2
64
2
        task.launchPath = "/bin/bash"
65
2
        task.arguments = ["-c", command]
66
2
67
2
        let stdoutPipe = Pipe()
68
2
        let stderrPipe = Pipe()
69
2
        task.standardOutput = stdoutPipe
70
2
        task.standardError = stderrPipe
71
2
72
2
        do {
73
2
            try task.run()
74
2
        } catch {
75
0
            return ["", "Error running: shell(command: \(command), stdin: \(String(describing: stdin))"]
76
2
        }
77
2
78
2
        let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
79
2
        let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
80
2
81
2
        let stdout = String(data: stdoutData, encoding: .utf8) ?? ""
82
2
        let stderr = String(data: stderrData, encoding: .utf8) ?? ""
83
2
84
2
        return [stdout, stderr]
85
2
    }
$s15CutBoxUnitTests13JSFuncServiceC5shellySaySSGSS_SSSgtXBvpfi
Line
Count
Source
52
392
    (String, String?) -> [String] = { command, stdin -> [String] in
53
392
        let task = Process()
54
392
55
392
        if let stdinString = stdin {
56
392
            let stdinPipe = Pipe()
57
392
            task.standardInput = stdinPipe
58
392
            if let stdinData = stdinString.data(using: .utf8) {
59
392
                stdinPipe.fileHandleForWriting.write(stdinData)
60
392
                stdinPipe.fileHandleForWriting.closeFile()
61
392
            }
62
392
        }
63
392
64
392
        task.launchPath = "/bin/bash"
65
392
        task.arguments = ["-c", command]
66
392
67
392
        let stdoutPipe = Pipe()
68
392
        let stderrPipe = Pipe()
69
392
        task.standardOutput = stdoutPipe
70
392
        task.standardError = stderrPipe
71
392
72
392
        do {
73
392
            try task.run()
74
392
        } catch {
75
392
            return ["", "Error running: shell(command: \(command), stdin: \(String(describing: stdin))"]
76
392
        }
77
392
78
392
        let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
79
392
        let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
80
392
81
392
        let stdout = String(data: stdoutData, encoding: .utf8) ?? ""
82
392
        let stderr = String(data: stderrData, encoding: .utf8) ?? ""
83
392
84
392
        return [stdout, stderr]
85
392
    }
$s15CutBoxUnitTests13JSFuncServiceC5shellySaySSGSS_SSSgtXBvpfiAESS_AFtcfU_
Line
Count
Source
52
2
    (String, String?) -> [String] = { command, stdin -> [String] in
53
2
        let task = Process()
54
2
55
2
        if let stdinString = stdin {
56
2
            let stdinPipe = Pipe()
57
2
            task.standardInput = stdinPipe
58
2
            if let stdinData = stdinString.data(using: .utf8) {
59
2
                stdinPipe.fileHandleForWriting.write(stdinData)
60
2
                stdinPipe.fileHandleForWriting.closeFile()
61
2
            }
62
2
        }
63
2
64
2
        task.launchPath = "/bin/bash"
65
2
        task.arguments = ["-c", command]
66
2
67
2
        let stdoutPipe = Pipe()
68
2
        let stderrPipe = Pipe()
69
2
        task.standardOutput = stdoutPipe
70
2
        task.standardError = stderrPipe
71
2
72
2
        do {
73
2
            try task.run()
74
2
        } catch {
75
0
            return ["", "Error running: shell(command: \(command), stdin: \(String(describing: stdin))"]
76
2
        }
77
2
78
2
        let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
79
2
        let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
80
2
81
2
        let stdout = String(data: stdoutData, encoding: .utf8) ?? ""
82
2
        let stderr = String(data: stderrData, encoding: .utf8) ?? ""
83
2
84
2
        return [stdout, stderr]
85
2
    }
86
87
    var filterText: String = ""
88
89
66
    var funcs: [(String, Int)] {
90
66
        guard let funcsDict: [[String: Any]] = js["cutboxFunctions"]
91
66
            .toArray() as? [[String: Any]] else {
92
40
                return []
93
40
        }
94
26
95
26
        var idx = 0
96
27
        return funcsDict.map { (dict: [String: Any]) -> (String, Int)? in
97
27
            if let name = dict["name"] as? String {
98
13
                let index = (name, idx)
99
13
                idx += 1
100
13
                return index
101
14
            } else {
102
14
                return nil
103
14
            }
104
0
        }
105
27
        .compactMap { $0 }
106
66
107
66
    }
108
109
59
    var funcList: [String] {
110
59
        let names: [String] = funcs.map { (t: (String, Int)) -> String in t.0 }
111
59
        if filterText.isEmpty {
112
58
            return names
113
58
        }
114
1
        return names.substringSearchFiltered(search: filterText)
115
59
    }
116
117
4
    func selected(name: String) -> (String, Int)? {
118
4
        guard let found = funcs.first(where: { $0.0 == name })
119
4
            else { return nil }
120
3
        return found
121
4
    }
122
123
392
    var js: JSContext = JSFuncService.context
124
125
77
    var count: Int {
126
77
        return self.funcList.count
127
77
    }
128
129
5
    var isEmpty: Bool {
130
5
        return self.funcList.isEmpty
131
5
    }
132
133
    var helpers = """
134
        help = `
135
        CutBox JS Help:
136
          cutboxFunctions - Array of loaded functions
137
138
        ${ls()}`
139
140
        var ls = () => cutboxFunctions.map(e => e.name).join(`\n`)
141
        var list = () => JSON.stringify(cutboxFunctions, null, 2)
142
        """
143
144
    var noCutboxJSHelp = """
145
        var help = `CutBox JS Help:\n
146
          ~/.cutbox.js does not exist\n
147
        Add and reload to paste through JS.
148
        See the wiki for more advanced info:
149
        https://github.com/cutbox/CutBox/wiki`
150
        """
151
152
27
    func setup() {
153
27
        self.js["require"] = self.require
154
27
        self.js["shellCommand"] = self.shellCommand
155
27
        self.js["shell"] = self.shell
156
27
    }
157
158
5
    func reload() {
159
5
        setup()
160
5
        repl(noCutboxJSHelp)
161
5
162
5
        guard let cutBoxJSLocation = prefs?.cutBoxJSLocation,
163
5
              let cutboxJS = getStringFromFile(cutBoxJSLocation) else {
164
2
            return
165
3
        }
166
3
167
3
        repl(helpers)
168
3
        repl(cutboxJS)
169
3
170
3
        if self.isEmpty {
171
2
            notifyUser(title: "Problem in cutbox js",
172
2
                       info: "Error: no function objects in cutboxFunctions")
173
3
        } else {
174
1
            notifyUser(title: "Javascript loaded",
175
1
                       info: "cutbox js loaded \(self.count) function(s)")
176
3
        }
177
3
    }
178
179
    @discardableResult
180
45
    func repl(_ line: String) -> String {
181
45
        return js.evaluateScript(line).toString()
182
45
    }
183
184
1
    func replValue(_ line: String) -> JSValue? {
185
1
        return js.evaluateScript(line)
186
1
    }
187
188
3
    func process(_ name: String, items: [String]) -> String {
189
3
        guard let index = selected(name: name)?.1
190
3
            else { return "" }
191
2
        return self.process(index, items: items)
192
3
    }
193
194
2
    func process(_ fnIndex: Int, items: [String]) -> String {
195
2
        return js.evaluateScript("cutboxFunctions[\(fnIndex)].fn")
196
2
            .call(withArguments: [items]).toString()!
197
2
    }
198
}
199
200
9
func getStringFromFile(_ expandedFilename: String) -> String? {
201
9
    guard let fileContent = try? String(contentsOfFile: expandedFilename)
202
9
    else { return nil }
203
6
    return fileContent
204
9
}