| @@ -36,6 +36,8 @@ public struct BabelJob: Sendable, Equatable { |
| 36 | var resolved: [String: Babel.VarValue] = [:] |
36 | var resolved: [String: Babel.VarValue] = [:] |
| 37 | var caret = 0 |
37 | var caret = 0 |
| 38 | var options = Babel.Options() |
38 | var options = Babel.Options() |
| |
39 | /// A script for the runner to write to an executable file, for `Babel.scriptFileToken`. |
| |
40 | public internal(set) var script: String? |
| 39 | /// An Emacs Lisp block's form, for `Babel.evaluateNatively`. |
41 | /// An Emacs Lisp block's form, for `Babel.evaluateNatively`. |
| 40 | public internal(set) var lispForm: String? |
42 | public internal(set) var lispForm: String? |
| 41 | |
43 | |
| @@ -91,6 +93,8 @@ public struct BabelOutput: Sendable, Equatable { |
| 91 | |
93 | |
| 92 | public enum Babel { |
94 | public enum Babel { |
| 93 | public static let resultFileToken = "__ORGSTAR_RESULT_FILE__" |
95 | public static let resultFileToken = "__ORGSTAR_RESULT_FILE__" |
| |
96 | /// Stands, in a job's arguments, for the executable file the runner writes `job.script` to. |
| |
97 | public static let scriptFileToken = "__ORGSTAR_SCRIPT_FILE__" |
| 94 | static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"] |
98 | static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"] |
| 95 | static let generic: [String: (String, [String])] = [ |
99 | static let generic: [String: (String, [String])] = [ |
| 96 | "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]), |
100 | "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]), |
| @@ -253,10 +257,11 @@ public enum Babel { |
| 253 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
257 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
| 254 | return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) |
258 | return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) |
| 255 | } |
259 | } |
| 256 | if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language) { |
260 | if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language), !shells.contains(language) { |
| 257 | return .failure(.message(":cmdline isn't supported yet; nothing was run.")) |
261 | return .failure(.message(":cmdline isn't supported for \(language) yet; nothing was run.")) |
| 258 | } |
262 | } |
| 259 | for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil { |
263 | for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil |
| |
264 | && !(shells.contains(language) && [":stdin", ":shebang"].contains(key)) { |
| 260 | return .failure(.message("\(key) isn't supported yet; nothing was run.")) |
265 | return .failure(.message("\(key) isn't supported yet; nothing was run.")) |
| 261 | } |
266 | } |
| 262 | let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) } |
267 | let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) } |
| @@ -285,8 +290,10 @@ public enum Babel { |
| 285 | file: toFile ? file : nil, fileDescription: params.single[":file-desc"]) |
290 | file: toFile ? file : nil, fileDescription: params.single[":file-desc"]) |
| 286 | } |
291 | } |
| 287 | let vars: [(String, VarValue)] |
292 | let vars: [(String, VarValue)] |
| |
293 | var stdin: VarValue? |
| 288 | do { |
294 | do { |
| 289 | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) } |
295 | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) } |
| |
296 | stdin = try params.single[":stdin"].map { try resolve($0, model: model, text: ns, refs: refs) } |
| 290 | } catch let needed as Needed { |
297 | } catch let needed as Needed { |
| 291 | // Asked about once, with every block the references reach. |
298 | // Asked about once, with every block the references reach. |
| 292 | let texts = [ns.substring(with: NSRange(location))] + referencedBlocks(params, model: model, text: ns, seen: []).map { ns.substring(with: NSRange($0.range)) } |
299 | let texts = [ns.substring(with: NSRange(location))] + referencedBlocks(params, model: model, text: ns, seen: []).map { ns.substring(with: NSRange($0.range)) } |
| @@ -314,8 +321,25 @@ public enum Babel { |
| 314 | } |
321 | } |
| 315 | lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'") |
322 | lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'") |
| 316 | } |
323 | } |
| 317 | let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "") |
| |
| 318 | let executable = language == "shell" ? options.shell : language |
324 | let executable = language == "shell" ? options.shell : language |
| |
325 | let cmdline = params.single[":cmdline"] |
| |
326 | let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil } |
| |
327 | if session == nil, stdin != nil || cmdline != nil || shebang != nil { |
| |
328 | // `org-babel-sh-evaluate`: an executable script, run by the shell with the |
| |
329 | // command line after it and `:stdin` as its input; the value is its last line. |
| |
330 | var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n" |
| |
331 | if params.single[":padline"] != "no" { script += "\n" } |
| |
332 | script += (lines + [body]).joined(separator: "\n") |
| |
333 | let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "") |
| |
334 | var job = BabelJob( |
| |
335 | language: language, runner: .process(executable: executable, arguments: ["-c", command]), |
| |
336 | input: stdin.map(shellString) ?? "", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, |
| |
337 | finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus)) |
| |
338 | ) |
| |
339 | job.script = script |
| |
340 | return .success(job) |
| |
341 | } |
| |
342 | let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "") |
| 319 | return .success(BabelJob( |
343 | return .success(BabelJob( |
| 320 | language: language, runner: session.map { .session(name: $0, executable: executable, python: false) } ?? .process(executable: executable, arguments: []), |
344 | language: language, runner: session.map { .session(name: $0, executable: executable, python: false) } ?? .process(executable: executable, arguments: []), |
| 321 | input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir, |
345 | input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir, |
| @@ -756,8 +780,8 @@ public enum Babel { |
| 756 | static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] { |
780 | static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] { |
| 757 | var found: [SrcBlockInfo] = [] |
781 | var found: [SrcBlockInfo] = [] |
| 758 | var seen = seen |
782 | var seen = seen |
| 759 | for variable in params.vars { |
783 | for value in params.vars.map(\.value) + [params.single[":stdin"]].compactMap({ $0 }) { |
| 760 | guard let (block, arguments) = referencedBlock(variable.value, model: model), let name = block.name, seen.insert(name).inserted else { continue } |
784 | guard let (block, arguments) = referencedBlock(value, model: model), let name = block.name, seen.insert(name).inserted else { continue } |
| 761 | found.append(block) |
785 | found.append(block) |
| 762 | let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] |
786 | let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] |
| 763 | if let inner = try? Params(block: block, model: model, text: text, extra: extra) { |
787 | if let inner = try? Params(block: block, model: model, text: text, extra: extra) { |
| @@ -820,6 +844,22 @@ public enum Babel { |
| 820 | throw Failure.message("\(value) isn't a table or list.") |
844 | throw Failure.message("\(value) isn't a table or list.") |
| 821 | } |
845 | } |
| 822 | |
846 | |
| |
847 | /// `org-babel-sh-var-to-string`: a table's rows as tab-separated lines, a list's items as |
| |
848 | /// lines. |
| |
849 | static func shellString(_ value: VarValue) -> String { |
| |
850 | func cell(_ c: Cell) -> String { |
| |
851 | switch c { |
| |
852 | case .number(let n): n |
| |
853 | case .text(let s): s |
| |
854 | } |
| |
855 | } |
| |
856 | switch value { |
| |
857 | case .scalar(let s), .number(let s): return s |
| |
858 | case .list(let items): return items.map(cell).joined(separator: "\n") |
| |
859 | case .table(let rows): return rows.map { $0.map(cell).joined(separator: "\t") }.joined(separator: "\n") |
| |
860 | } |
| |
861 | } |
| |
862 | |
| 823 | /// `org-babel-python-var-to-python`. |
863 | /// `org-babel-python-var-to-python`. |
| 824 | static func pythonLiteral(_ value: VarValue) -> String { |
864 | static func pythonLiteral(_ value: VarValue) -> String { |
| 825 | func cell(_ c: Cell) -> String { |
865 | func cell(_ c: Cell) -> String { |