Sources/OrgCore/Compute/Babel.swift
935 lines · 44277 bytes
1import Foundation
2
3// Babel (ob-core.el, ob-shell.el, ob-python.el, ob-emacs-lisp.el, Org 9.8.7): executing a src
4// block is planned here, run by the platform, and finished here. The plan resolves header
5// arguments and refuses anything that would make the run differ from Emacs's; the finish turns
6// the output into a result the way `org-babel-insert-result` writes it.
7
8/// A src block run for the platform to carry out.
9public struct BabelJob: Sendable, Equatable {
10 public enum Runner: Sendable, Equatable {
11 /// An interpreter reading the program on standard input.
12 case process(executable: String, arguments: [String])
13 /// `emacs --batch` loading `input` as a script.
14 case emacs
15 }
16
17 public let language: String
18 public let runner: Runner
19 /// Standard input, or the Emacs script. `Babel.resultFileToken` stands for a file the runner
20 /// creates; its contents come back as `BabelOutput.resultFile`.
21 public let input: String
22 /// `:dir`, relative to the file's folder; nil for the folder itself.
23 public let directory: String?
24 /// The block as it was planned, to find it again when the run ends.
25 public let block: Range<Int>
26 public let blockText: String
27 /// Asks every time (`:eval query`) rather than trusting by content.
28 public let alwaysAsk: Bool
29 public let finish: Finish
30
31 public struct Finish: Sendable, Equatable {
32 enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp }
33 let kind: Kind
34 var language = ""
35 public let params: [String]
36 let output: Bool
37 let wrap: String?
38 let name: String?
39 }
40}
41
42public struct BabelOutput: Sendable, Equatable {
43 public var stdout: String
44 public var resultFile: String?
45 public init(stdout: String, resultFile: String? = nil) {
46 self.stdout = stdout
47 self.resultFile = resultFile
48 }
49}
50
51public enum Babel {
52 public static let resultFileToken = "__ORGSTAR_RESULT_FILE__"
53 static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"]
54 static let generic: [String: (String, [String])] = [
55 "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]),
56 "awk": ("awk", ["-f", "/dev/stdin"]),
57 ]
58
59 public struct Options: Sendable, Equatable {
60 public var python = "python3"
61 /// `shell-file-name`, for blocks in the `shell` language.
62 public var shell = "/bin/sh"
63 public init() {}
64 }
65
66 // MARK: - Planning
67
68 /// The run for the src block at the caret, or why it can't run.
69 public static func plan(_ context: EditContext, options: Options = Options()) -> Result<BabelJob, Failure> {
70 let text = context.text
71 let ns = text as NSString
72 let model = DocumentModel(tree: context.tree)
73 guard let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) else {
74 return .failure(.message("Not in a source block"))
75 }
76 guard let language = block.language else { return .failure(.message("This block has no language")) }
77 let params: Params
78 do {
79 params = try Params(block: block, model: model, text: ns)
80 } catch let error as Failure {
81 return .failure(error)
82 } catch {
83 return .failure(.message("\(error)"))
84 }
85 let eval = params.single[":eval"]?.lowercased()
86 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
87 return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled."))
88 }
89 for key in [":prologue", ":epilogue", ":file", ":post", ":stdin", ":cmdline", ":shebang"] where params.single[key] != nil {
90 return .failure(.message("\(key) isn't supported yet; nothing was run."))
91 }
92 if let session = params.single[":session"], session != "none" { return .failure(.message(":session isn't supported yet; nothing was run.")) }
93 if let noweb = params.single[":noweb"], !["no", "strip-export", "tangle"].contains(noweb) {
94 return .failure(.message(":noweb isn't supported yet; nothing was run."))
95 }
96 if params.single[":cache"] == "yes" { return .failure(.message(":cache isn't supported yet; nothing was run.")) }
97
98 let body = normalizedBody(block)
99 let output = params.results.contains("output")
100 let finishBase = { (kind: BabelJob.Finish.Kind) in
101 BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: block.name)
102 }
103 let vars: [(String, VarValue)]
104 do {
105 vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns)) }
106 } catch let error as Failure {
107 return .failure(error)
108 } catch {
109 return .failure(.message("\(error)"))
110 }
111 let blockText = ns.substring(with: NSRange(block.range))
112 let dir = params.single[":dir"]
113 let alwaysAsk = eval == "query"
114
115 if shells.contains(language) {
116 let valueIsExitStatus = params.results.contains("value")
117 var lines: [String] = []
118 for (name, value) in vars {
119 let s: String
120 switch value {
121 case .scalar(let v), .number(let v): s = v
122 default: return .failure(.message("Tables and lists in :var aren't supported for \(language) yet."))
123 }
124 lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'")
125 }
126 let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
127 let executable = language == "shell" ? options.shell : language
128 return .success(BabelJob(
129 language: language, runner: .process(executable: executable, arguments: []),
130 input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir,
131 block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
132 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
133 ))
134 }
135 if language == "python" {
136 let lines = vars.map { "\($0.0)=\(pythonLiteral($0.1))" }
137 var full = (lines + [body]).joined(separator: "\n")
138 if let returnValue = params.single[":return"], !output { full += "\nreturn \(returnValue)" }
139 let input: String
140 if output {
141 input = full + "\n"
142 } else {
143 input = pythonFormatter + "def main():\n" + shiftRight(full) + "\n\n__orgstar_format(main(), '\(resultFileToken)', ["
144 + params.results.map { "\"\($0)\"" }.joined(separator: ", ") + "])\n"
145 }
146 return .success(BabelJob(
147 language: language, runner: .process(executable: params.single[":python"] ?? options.python, arguments: []),
148 input: input, directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
149 finish: finishBase(.python)
150 ))
151 }
152 if language == "emacs-lisp" || language == "elisp" {
153 let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" }
154 let expanded = bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n ")))\n\(body)\n)"
155 let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)"
156 return .success(BabelJob(
157 language: language, runner: .emacs, input: emacsScript(form: form, params: params.results),
158 directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp)
159 ))
160 }
161 if let (executable, arguments) = generic[language] {
162 guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) }
163 return .success(BabelJob(
164 language: language, runner: .process(executable: params.single[":cmd"] ?? executable, arguments: arguments),
165 input: body + "\n", directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
166 finish: BabelJob.Finish(kind: .generic, language: language, params: params.results + (output ? [] : ["output"]), output: true, wrap: params.wrap, name: block.name)
167 ))
168 }
169 return .failure(.message("No way to run \(language) blocks yet."))
170 }
171
172 public enum Failure: Error, Equatable {
173 case message(String)
174 }
175
176 /// `org-babel--normalize-body`: without the final newline, and without the indentation
177 /// common to its lines unless `-i` keeps it.
178 static func normalizedBody(_ block: SrcBlockInfo) -> String {
179 var lines = block.body.components(separatedBy: "\n")
180 if lines.last == "" { lines.removeLast() }
181 // Org escapes lines starting with `*` or `#+` inside blocks with a comma.
182 lines = lines.map { $0.replacingOccurrences(of: "^([ \\t]*),([,*]|#\\+)", with: "$1$2", options: .regularExpression) }
183 if !block.switches.contains("-i") {
184 let indents = lines.filter { $0.contains(where: { !$0.isWhitespace }) }.map { $0.prefix { $0 == " " || $0 == "\t" }.count }
185 let common = indents.min() ?? 0
186 lines = lines.map { String($0.dropFirst(min(common, $0.prefix { $0 == " " || $0 == "\t" }.count))) }
187 }
188 return lines.joined(separator: "\n")
189 }
190
191 // MARK: - Header arguments
192
193 struct Params {
194 var results: [String] = ["replace"]
195 var single: [String: String] = [:]
196 var vars: [(name: String, value: String)] = []
197 var wrap: String?
198
199 static let resultGroups: [Set<String>] = [
200 ["file", "list", "vector", "table", "scalar", "verbatim"],
201 ["raw", "html", "latex", "org", "code", "pp", "drawer", "link", "graphics"],
202 ["replace", "silent", "none", "discard", "append", "prepend"],
203 ["output", "value"],
204 ]
205
206 init(block: SrcBlockInfo, model: DocumentModel, text: NSString) throws {
207 var layers: [[(String, String)]] = []
208 let language = block.language ?? ""
209 func properties(_ entries: [Property]) {
210 for key in ["header-args", "header-args:\(language)"] {
211 for entry in entries where entry.key.lowercased() == key {
212 let pairs = Params.parse(entry.value)
213 layers.append(pairs)
214 }
215 }
216 }
217 properties(model.fileProperties)
218 if let heading = block.heading {
219 for index in model.ancestors(of: heading) + [heading] { properties(model.headings[index].properties) }
220 }
221 let firstLine = text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
222 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
223 var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
224 while let first = rest.split(separator: " ").first, first.hasPrefix("-") || first.hasPrefix("+") {
225 rest = String(rest.dropFirst(first.count)).trimmingCharacters(in: .whitespaces)
226 }
227 layers.append(Params.parse(rest))
228 }
229 // `#+HEADER:` lines above the block.
230 var headers: [String] = []
231 var location = block.range.lowerBound
232 while location > 0 {
233 let previous = text.lineRange(for: NSRange(location: location - 1, length: 0))
234 let line = text.substring(with: previous).trimmingCharacters(in: .whitespacesAndNewlines)
235 guard line.range(of: "(?i)^#\\+(header|name|caption|attr_[a-z]+|results|plot|label):", options: .regularExpression) != nil else { break }
236 if line.lowercased().hasPrefix("#+header:") { headers.insert(String(line.dropFirst("#+header:".count)), at: 0) }
237 location = previous.location
238 }
239 for header in headers { layers.append(Params.parse(header)) }
240
241 for layer in layers {
242 for (key, value) in layer {
243 switch key {
244 case ":results":
245 for word in value.split(whereSeparator: \.isWhitespace).map(String.init) {
246 if let group = Params.resultGroups.first(where: { $0.contains(word) }) { results.removeAll { group.contains($0) } }
247 results.removeAll { $0 == word }
248 results.insert(word, at: 0)
249 }
250 case ":var":
251 guard let eq = value.firstIndex(of: "=") else { throw Failure.message(":var \(value) needs a name and a value") }
252 let name = value[..<eq].trimmingCharacters(in: .whitespaces)
253 let assigned = value[value.index(after: eq)...].trimmingCharacters(in: .whitespaces)
254 vars.removeAll { $0.name == name }
255 vars.append((name, assigned))
256 case ":wrap":
257 wrap = value
258 default:
259 single[key] = value
260 }
261 }
262 }
263 }
264
265 /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and
266 /// parentheses.
267 static func parse(_ text: String) -> [(String, String)] {
268 var parts: [String] = []
269 var current = ""
270 var depth = 0
271 var quoted = false
272 var previous: Character = " "
273 for c in text {
274 if c == "\"" { quoted.toggle() }
275 if !quoted {
276 if c == "(" { depth += 1 }
277 if c == ")" { depth = max(0, depth - 1) }
278 }
279 if c == ":", !quoted, depth == 0, previous == " " || previous == "\t" {
280 parts.append(current)
281 current = ""
282 }
283 current.append(c)
284 previous = c
285 }
286 parts.append(current)
287 var pairs: [(String, String)] = []
288 for part in parts {
289 let trimmed = part.trimmingCharacters(in: .whitespaces)
290 guard trimmed.hasPrefix(":") else { continue }
291 let key = String(trimmed.prefix { !$0.isWhitespace }).lowercased()
292 var value = trimmed.dropFirst(key.count).trimmingCharacters(in: .whitespaces)
293 if key != ":var", value.count >= 2, value.hasPrefix("\""), value.hasSuffix("\"") {
294 value = String(value.dropFirst().dropLast())
295 }
296 pairs.append((key, value))
297 }
298 return pairs
299 }
300 }
301
302 // MARK: - Variables
303
304 enum VarValue: Equatable {
305 case scalar(String)
306 case number(String)
307 case table([[Cell]])
308 case list([Cell])
309 }
310
311 enum Cell: Equatable {
312 case text(String)
313 case number(String)
314 }
315
316 /// `org-babel--string-to-number`, keeping Emacs's printed form.
317 static func number(_ s: String) -> String? {
318 let t = s.trimmingCharacters(in: .whitespaces)
319 guard !t.isEmpty, !t.contains(where: \.isWhitespace), t.range(of: "^[0-9e.+ -]+$", options: .regularExpression) != nil else { return nil }
320 if t.range(of: "^[-+]?[0-9]+\\.?$", options: .regularExpression) != nil {
321 var digits = t.hasSuffix(".") ? String(t.dropLast()) : t
322 let negative = digits.hasPrefix("-")
323 if digits.hasPrefix("-") || digits.hasPrefix("+") { digits.removeFirst() }
324 digits = String(digits.drop { $0 == "0" })
325 if digits.isEmpty { return "0" }
326 return (negative ? "-" : "") + digits
327 }
328 guard t.range(of: "^[-+]?([0-9]+\\.[0-9]+|[0-9]*\\.[0-9]+|[0-9]+(\\.[0-9]*)?e[-+]?[0-9]+|[0-9]*\\.[0-9]+e[-+]?[0-9]+)$", options: .regularExpression) != nil,
329 let d = Double(t) else { return nil }
330 return emacsFloat(d)
331 }
332
333 /// How Emacs prints a float.
334 static func emacsFloat(_ d: Double) -> String {
335 if d == d.rounded(), abs(d) < 1e16 { return String(format: "%.1f", d) }
336 return "\(d)"
337 }
338
339 static func cell(_ s: String) -> Cell {
340 var t = s
341 if let m = s.firstMatch(of: /^\s*"(.+)"\s*$/) { t = String(m.1) }
342 return number(t).map(Cell.number) ?? .text(t)
343 }
344
345 /// `org-babel-ref-resolve` for literals and named tables and lists in the file.
346 static func resolve(_ value: String, model: DocumentModel, text: NSString) throws -> VarValue {
347 if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(String(value.dropFirst().dropLast())) }
348 if let n = number(value) { return .number(n) }
349 if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") }
350 // A named element: `#+NAME: value` above a table or list.
351 let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$"
352 guard let r = (text as String).range(of: pattern, options: .regularExpression) else {
353 throw Failure.message("Can't find \(value) for :var.")
354 }
355 let start = NSMaxRange(text.lineRange(for: NSRange(r, in: text as String)))
356 var location = start
357 var rows: [[Cell]] = []
358 var items: [Cell] = []
359 while location < text.length {
360 let lineRange = text.lineRange(for: NSRange(location: location, length: 0))
361 let line = text.substring(with: lineRange).trimmingCharacters(in: .newlines)
362 let trimmed = line.trimmingCharacters(in: .whitespaces)
363 if trimmed.hasPrefix("|") {
364 if trimmed.hasPrefix("|-") { throw Failure.message("Tables with rules in :var aren't supported yet.") }
365 let cells = trimmed.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
366 rows.append((trimmed.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell))
367 } else if let m = trimmed.firstMatch(of: /^[-+*] (.*)$/), rows.isEmpty {
368 items.append(cell(String(m.1)))
369 } else if trimmed.hasPrefix("#+") || (!rows.isEmpty || !items.isEmpty) {
370 if rows.isEmpty, items.isEmpty, trimmed.lowercased().hasPrefix("#+begin_src") {
371 throw Failure.message("Using a src block's result in :var isn't supported yet.")
372 }
373 break
374 } else {
375 break
376 }
377 location = NSMaxRange(lineRange)
378 }
379 if !rows.isEmpty { return .table(rows) }
380 if !items.isEmpty { return .list(items) }
381 throw Failure.message("\(value) isn't a table or list.")
382 }
383
384 /// `org-babel-python-var-to-python`.
385 static func pythonLiteral(_ value: VarValue) -> String {
386 func cell(_ c: Cell) -> String {
387 switch c {
388 case .number(let n): return n
389 case .text(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
390 }
391 }
392 switch value {
393 case .scalar(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
394 case .number(let n): return n
395 case .list(let items): return "[" + items.map(cell).joined(separator: ", ") + "]"
396 case .table(let rows): return "[" + rows.map { "[" + $0.map(cell).joined(separator: ", ") + "]" }.joined(separator: ", ") + "]"
397 }
398 }
399
400 /// `prin1` of a string.
401 static func lispString(_ s: String, triple: Bool = false) -> String {
402 let quoted = "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
403 return triple ? "\"\"" + quoted + "\"\"" : quoted
404 }
405
406 static func lispLiteral(_ value: VarValue) -> String {
407 func cell(_ c: Cell) -> String {
408 switch c {
409 case .number(let n): return n
410 case .text(let s): return lispString(s)
411 }
412 }
413 switch value {
414 case .scalar(let s): return lispString(s)
415 case .number(let n): return n
416 case .list(let items): return "(" + items.map(cell).joined(separator: " ") + ")"
417 case .table(let rows): return "(" + rows.map { "(" + $0.map(cell).joined(separator: " ") + ")" }.joined(separator: " ") + ")"
418 }
419 }
420
421 // MARK: - Python
422
423 /// Writes the value of `main()` to the result file as org's Python wrapper does: `pprint`
424 /// for `pp`, `key :: value` items for a dict as a list, dicts as rows for tables,
425 /// otherwise `str`.
426 static let pythonFormatter = """
427 def __orgstar_format(value, path, params):
428 def rows(v):
429 if isinstance(v, dict):
430 return [(k, rows(x)) for k, x in v.items()]
431 if isinstance(v, (list, tuple)):
432 return [rows(x) for x in v]
433 return v
434 with open(path, 'w') as out:
435 if 'pp' in params:
436 import pprint
437 out.write(pprint.pformat(value))
438 elif 'list' in params and isinstance(value, dict):
439 out.write(str(['{} :: {}'.format(k, v) for k, v in value.items()]))
440 else:
441 if not set(params) & {'scalar', 'verbatim', 'raw'} and 'table' in params:
442 value = rows(value)
443 out.write(str(value))
444
445 """
446
447 /// `org-babel-python--shift-right`: four spaces more on every line outside strings.
448 static func shiftRight(_ body: String) -> String {
449 var inString: String?
450 var lines: [String] = []
451 for line in body.components(separatedBy: "\n") {
452 lines.append(inString == nil ? " " + line : line)
453 var i = line.startIndex
454 while i < line.endIndex {
455 if let delimiter = inString {
456 if line[i...].hasPrefix(delimiter) {
457 inString = nil
458 i = line.index(i, offsetBy: 3)
459 continue
460 }
461 } else if line[i] == "#" {
462 break
463 } else if line[i...].hasPrefix("\"\"\"") || line[i...].hasPrefix("'''") {
464 inString = String(line[i...].prefix(3))
465 i = line.index(i, offsetBy: 3)
466 continue
467 } else if line[i] == "\"" || line[i] == "'" {
468 let quote = line[i]
469 i = line.index(after: i)
470 while i < line.endIndex, line[i] != quote {
471 if line[i] == "\\" { i = line.index(after: i) }
472 if i < line.endIndex { i = line.index(after: i) }
473 }
474 }
475 if i < line.endIndex { i = line.index(after: i) }
476 }
477 }
478 return lines.joined(separator: "\n")
479 }
480
481 // MARK: - Emacs Lisp
482
483 /// Evaluates `form` and prints the result as JSON: its `%s` and `%S` forms, and its rows
484 /// when it is a table.
485 static func emacsScript(form: String, params: [String]) -> String {
486 """
487 ;;; -*- lexical-binding: t -*-
488 (require 'cl-lib)
489 (require 'json)
490 (let* ((result \(form))
491 (print-level nil)
492 (print-length nil)
493 (cell (lambda (c) (if (stringp c) c (format "%s" c))))
494 (tabulable (and (proper-list-p result)
495 (cl-every (lambda (e) (or (atom e) (proper-list-p e))) result)))
496 (rows (and tabulable result
497 (if (cl-every (lambda (e) (or (eq e 'hline) (listp e))) result)
498 (mapcar (lambda (r) (if (eq r 'hline) "hline" (vconcat (mapcar cell r)))) result)
499 (list (vconcat (mapcar cell result)))))))
500 (princ (json-encode `((s . ,(format "%s" result))
501 (S . ,(format "%S" result))
502 (string . ,(if (stringp result) t :json-false))
503 (rows . ,(if rows (vconcat rows) :json-null))))))
504 """
505 }
506
507 // MARK: - Finishing
508
509 /// What the run produced, as Babel holds it before inserting.
510 enum Value: Equatable {
511 case none
512 case string(String)
513 /// Rows of cells; nil rows are rules.
514 case table([[String]?])
515 }
516
517 /// The edit that writes the run's result under the block, or why it can't.
518 public static func finish(_ job: BabelJob, output: BabelOutput, text: String, settings: OrgSettings = .default) -> Result<[TextEdit], Failure> {
519 let ns = text as NSString
520 var blockRange = job.block
521 if blockRange.upperBound > ns.length || ns.substring(with: NSRange(blockRange)) != job.blockText {
522 let found = ns.range(of: job.blockText)
523 guard found.location != NSNotFound,
524 ns.range(of: job.blockText, range: NSRange(location: NSMaxRange(found), length: ns.length - NSMaxRange(found))).location == NSNotFound else {
525 return .failure(.message("The block changed while it ran; the result wasn't written."))
526 }
527 blockRange = found.location..<NSMaxRange(found)
528 }
529 let value = Self.value(job.finish, output)
530 if job.finish.params.contains("silent") || job.finish.params.contains("none") || job.finish.params.contains("discard") { return .success([]) }
531 let buffer = EmacsBuffer(text, point: 0, settings: settings)
532 insertResult(value, job.finish, block: blockRange, buffer: buffer)
533 return .success(TextEdit.between(text, buffer.text as String))
534 }
535
536 /// The language's result handling: `org-babel-result-cond` and the table import.
537 static func value(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value {
538 let params = finish.params
539 let scalar = params.contains("scalar") || params.contains("verbatim") || params.contains("html") || params.contains("code")
540 || params.contains("pp") || params.contains("file")
541 || ((params.contains("output") || params.contains("raw") || params.contains("org")) && !params.contains("table"))
542 switch finish.kind {
543 case .shell(let valueIsExitStatus):
544 var results = output.stdout
545 if valueIsExitStatus {
546 results = results.split(separator: "\n", omittingEmptySubsequences: true).last.map(String.init) ?? ""
547 }
548 if scalar { return .string(results) }
549 return importTable(results)
550 case .generic:
551 return .string(output.stdout)
552 case .python:
553 let raw = finish.output ? output.stdout : (output.resultFile ?? "")
554 if scalar { return .string(raw) }
555 return pythonValue(raw)
556 case .emacsLisp:
557 guard let data = output.stdout.data(using: .utf8),
558 let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return .string(output.stdout) }
559 if scalar {
560 return .string((params.contains("scalar") || params.contains("verbatim") ? object["S"] : object["s"]) as? String ?? "")
561 }
562 if object["string"] as? Bool == true { return .string(object["s"] as? String ?? "") }
563 if let rows = object["rows"] as? [Any] {
564 return .table(rows.map { $0 as? String == "hline" ? nil : ($0 as? [String] ?? []) })
565 }
566 let printed = object["S"] as? String ?? ""
567 return printed == "nil" ? .none : .string(printed)
568 }
569 }
570
571 /// `org-babel-import-elisp-from-file`.
572 static func importTable(_ results: String) -> Value {
573 guard !results.isEmpty else { return .none }
574 var lines = results.components(separatedBy: "\n")
575 if lines.last == "" { lines.removeLast() }
576 let nonEmpty = lines.filter { !$0.isEmpty }
577 let rows: [[String]]
578 if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains("\t") }) {
579 rows = lines.map { $0.components(separatedBy: "\t") }
580 } else if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains(",") }) {
581 rows = lines.map(csvFields)
582 } else if lines.count == 1 {
583 rows = [[lines[0]]]
584 } else {
585 rows = lines.map { line in
586 let trimmed = line.drop { $0 == " " }
587 return trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init).isEmpty
588 ? [""] : trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
589 }
590 }
591 let cells = rows.map { row in row.map { field -> String in
592 let trimmed = field.trimmingCharacters(in: .whitespaces)
593 switch cell(trimmed) {
594 case .number(let n): return n
595 case .text(let t): return t
596 }
597 } }
598 if cells.count == 1, cells[0].count == 1 { return .string(cells[0][0]) }
599 return .table(cells)
600 }
601
602 static func csvFields(_ line: String) -> [String] {
603 var fields: [String] = []
604 var current = ""
605 var quoted = false
606 for c in line {
607 if c == "\"" {
608 quoted.toggle()
609 } else if c == ",", !quoted {
610 fields.append(current.trimmingCharacters(in: .whitespaces))
611 current = ""
612 } else {
613 current.append(c)
614 }
615 }
616 fields.append(current.trimmingCharacters(in: .whitespaces))
617 return fields
618 }
619
620 /// `org-babel-python-table-or-string`.
621 static func pythonValue(_ raw: String) -> Value {
622 guard let first = raw.first, let last = raw.last, raw.count >= 2,
623 (first == "[" && last == "]") || (first == "(" && last == ")") else {
624 if raw.isEmpty { return .string("") }
625 if raw.count > 2, (raw.hasPrefix("'") && raw.hasSuffix("'")) || (raw.hasPrefix("\"") && raw.hasSuffix("\"")) {
626 return .string(String(raw.dropFirst().dropLast()))
627 }
628 if let n = number(raw) { return .string(n) }
629 return .string(raw)
630 }
631 var parser = PythonLiteral(Array(raw))
632 guard let parsed = parser.parse(), parser.atEnd, case .list(let items) = parsed else { return .string(raw) }
633 func text(_ v: PythonLiteral.Node) -> String {
634 switch v {
635 case .atom(let s): return s
636 case .string(let s): return s
637 case .list(let xs): return "(" + xs.map(text).joined(separator: " ") + ")"
638 }
639 }
640 if items.allSatisfy({ if case .list = $0 { return true }; if case .atom("None") = $0 { return true }; return false }) {
641 return .table(items.map { item -> [String]? in
642 if case .list(let cells) = item { return cells.map(text) }
643 return nil
644 })
645 }
646 return .table([items.map { if case .atom("None") = $0 { return "hline" }; return text($0) }])
647 }
648
649 struct PythonLiteral {
650 indirect enum Node: Equatable {
651 case atom(String)
652 case string(String)
653 case list([Node])
654 }
655
656 let chars: [Character]
657 var i = 0
658 init(_ chars: [Character]) { self.chars = chars }
659 var atEnd: Bool { mutating get { skip(); return i == chars.count } }
660
661 mutating func skip() { while i < chars.count, chars[i] == " " || chars[i] == "\n" { i += 1 } }
662
663 mutating func parse() -> Node? {
664 skip()
665 guard i < chars.count else { return nil }
666 let c = chars[i]
667 if c == "[" || c == "(" {
668 let close: Character = c == "[" ? "]" : ")"
669 i += 1
670 var items: [Node] = []
671 skip()
672 if i < chars.count, chars[i] == close {
673 i += 1
674 return .list(items)
675 }
676 while true {
677 guard let item = parse() else { return nil }
678 items.append(item)
679 skip()
680 guard i < chars.count else { return nil }
681 if chars[i] == "," {
682 i += 1
683 skip()
684 if i < chars.count, chars[i] == close {
685 i += 1
686 return .list(items)
687 }
688 continue
689 }
690 if chars[i] == close {
691 i += 1
692 return .list(items)
693 }
694 return nil
695 }
696 }
697 if c == "'" || c == "\"" {
698 i += 1
699 var s = ""
700 while i < chars.count, chars[i] != c {
701 if chars[i] == "\\", i + 1 < chars.count {
702 i += 1
703 switch chars[i] {
704 case "n": s.append("\n")
705 case "t": s.append("\t")
706 default: s.append(chars[i])
707 }
708 } else {
709 s.append(chars[i])
710 }
711 i += 1
712 }
713 guard i < chars.count else { return nil }
714 i += 1
715 return .string(s)
716 }
717 var s = ""
718 while i < chars.count, !",)] \n".contains(chars[i]) {
719 s.append(chars[i])
720 i += 1
721 }
722 guard !s.isEmpty else { return nil }
723 return .atom(number(s) ?? s)
724 }
725 }
726
727 // MARK: - Inserting
728
729 static let resultKeyword = "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:"
730
731 /// `org-babel-where-is-src-block-result` with `insert`: the line of the block's results
732 /// keyword, added when there is none.
733 static func resultsLine(_ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) -> Int {
734 let ns = buffer.text
735 if let name = finish.name {
736 let pattern = "(?im)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*" + NSRegularExpression.escapedPattern(for: name) + "[ \\t]*$"
737 let regex = try! NSRegularExpression(pattern: pattern)
738 for m in regex.matches(in: ns as String, range: NSRange(location: 0, length: ns.length)) {
739 // Not the name of another block.
740 return m.range.location
741 }
742 } else {
743 // The element after the block, past blank lines.
744 var location = block.upperBound
745 while location < ns.length {
746 let line = ns.lineRange(for: NSRange(location: location, length: 0))
747 let content = ns.substring(with: line)
748 if content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
749 location = NSMaxRange(line)
750 continue
751 }
752 if content.range(of: "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*\\n?$", options: .regularExpression) != nil {
753 return line.location
754 }
755 break
756 }
757 }
758 // A new keyword after the block.
759 buffer.point = min(block.upperBound, buffer.length)
760 buffer.skipBackward([32, 9, 10, 13])
761 buffer.forwardLine(1)
762 if !buffer.bolp { buffer.insert("\n") }
763 let indent = buffer.saveExcursion { () -> String in
764 buffer.point = block.lowerBound
765 let line = buffer.substring(buffer.lineStart(buffer.point), buffer.lineEnd(buffer.point))
766 return String(line.prefix { $0 == " " || $0 == "\t" })
767 }
768 buffer.insert("\n")
769 let keywordLine = buffer.point
770 buffer.insert(indent + "#+RESULTS:" + (finish.name.map { " " + $0 } ?? "") + "\n")
771 if buffer.looking("^[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil {
772 buffer.saveExcursion { buffer.insert("\n") }
773 }
774 return keywordLine
775 }
776
777 /// `org-babel-result-end` with point at the start of the result.
778 static func resultEnd(_ buffer: EmacsBuffer) -> Int {
779 let start = buffer.point
780 let line = buffer.substring(buffer.lineStart(start), buffer.lineEnd(start))
781 if line.trimmingCharacters(in: .whitespaces).isEmpty { return start }
782 if line.range(of: "^[ \\t]*\\[\\[[^\\]]+\\](\\[[^\\]]*\\])?\\][ \\t]*$", options: .regularExpression) != nil {
783 return min(buffer.length, buffer.lineEnd(start) + 1)
784 }
785 let tree = OrgParser.parse(buffer.text as String)
786 var node = tree.root
787 var found: SyntaxNode?
788 while let child = node.child(containing: start) {
789 if child.range.lowerBound == start || (buffer.lineStart(child.range.lowerBound) == start) {
790 if [.fixedWidth, .table, .plainList, .drawer, .block, .dynamicBlock, .latexFragment].contains(child.kind) {
791 found = child
792 break
793 }
794 }
795 node = child
796 }
797 guard let element = found else { return start }
798 var end = element.range.upperBound
799 while end > start, let c = buffer.character(at: end - 1), c == 32 || c == 9 || c == 10 || c == 13 { end -= 1 }
800 return min(buffer.length, buffer.lineEnd(end) + 1)
801 }
802
803 /// `org-babel-insert-result` for a src block.
804 static func insertResult(_ value: Value, _ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) {
805 let params = finish.params
806 let keyword = resultsLine(finish, block: block, buffer: buffer)
807 buffer.point = keyword
808 let indent = buffer.substring(buffer.lineStart(keyword), buffer.lineEnd(keyword)).prefix { $0 == " " || $0 == "\t" }.count
809 buffer.forwardLine(1)
810 if buffer.point == buffer.lineEnd(keyword) { buffer.insert("\n") }
811 var beg = buffer.point
812 if params.contains("replace") {
813 buffer.deleteRegion(beg, resultEnd(buffer))
814 buffer.point = beg
815 } else if params.contains("append") {
816 buffer.point = resultEnd(buffer)
817 beg = buffer.point
818 }
819 var tabulated = false
820 switch value {
821 case .none:
822 break
823 case .string(var s) where params.contains("list"):
824 if s.last != "\n" { s += "\n" }
825 let items = s.split(separator: "\n", omittingEmptySubsequences: true)
826 buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
827 case .table(let rows) where params.contains("list"):
828 let items = rows.count == 1 ? (rows[0] ?? []) : rows.compactMap { $0?.joined(separator: " ") }
829 buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
830 case .table(let rows):
831 tabulated = true
832 let lines = rows.map { row in row.map { "| " + $0.map { " " + $0 + " " }.joined(separator: "|").dropFirst() + " |" } ?? "|-" }
833 let start = buffer.point
834 buffer.insert(lines.joined(separator: "\n") + "\n")
835 buffer.point = start
836 buffer.tableAlign()
837 buffer.point = buffer.tableEnd()
838 case .string(var s):
839 if !s.isEmpty, s.last != "\n" { s += "\n" }
840 buffer.insert(s)
841 }
842 let end = buffer.marker(buffer.point, advances: true)
843 defer { buffer.release(end) }
844 func wrap(_ open: String, _ close: String, escape: Bool) {
845 buffer.point = end.position
846 buffer.insert(close + "\n")
847 buffer.point = beg
848 buffer.insert(open + "\n")
849 if escape { escapeCode(buffer, from: buffer.point, to: end.position - close.utf16.count - 1) }
850 }
851 if let wrapValue = finish.wrap {
852 let full = wrapValue.isEmpty ? "results" : wrapValue
853 let type = String(full.split(separator: " ").first ?? "results")
854 if type.lowercased() != "nil", type.lowercased() != "no" {
855 wrap("#+begin_" + full, "#+end_" + type, escape: !["example", "src", "export"].contains(type.lowercased()) ? false : true)
856 }
857 } else if params.contains("html") {
858 wrap("#+begin_export html", "#+end_export", escape: true)
859 } else if params.contains("latex") {
860 wrap("#+begin_export latex", "#+end_export", escape: true)
861 } else if params.contains("org") {
862 wrap("#+begin_src org", "#+end_src", escape: true)
863 } else if params.contains("code") {
864 wrap("#+begin_src \(finish.language.isEmpty ? "none" : finish.language)", "#+end_src", escape: true)
865 } else if params.contains("raw") {
866 // As inserted.
867 } else if params.contains("drawer") {
868 wrap(":results:", ":end:", escape: false)
869 } else if !tabulated, !params.contains("list") {
870 examplify(buffer, beg, end.position)
871 }
872 if indent > 0, !(tabulated && params.contains("append")) {
873 buffer.point = beg
874 while buffer.point < end.position {
875 if !buffer.eolp { buffer.insert(String(repeating: " ", count: indent)) }
876 if buffer.forwardLine(1) != 0 { break }
877 }
878 }
879 }
880
881 /// `org-escape-code-in-region`.
882 static func escapeCode(_ buffer: EmacsBuffer, from start: Int, to end: Int) {
883 buffer.point = start
884 let stop = buffer.marker(end)
885 defer { buffer.release(stop) }
886 while buffer.point < stop.position {
887 let line = buffer.substring(buffer.point, buffer.lineEnd(buffer.point))
888 if let m = line.firstMatch(of: /^[ \t]*(,*(\*|#\+))/) {
889 buffer.point += line[..<m.1.startIndex].utf16.count
890 buffer.insert(",")
891 }
892 if buffer.forwardLine(1) != 0 { break }
893 }
894 }
895
896 /// `org-babel-examplify-region`: fewer than ten lines get `: `, more an example block.
897 static func examplify(_ buffer: EmacsBuffer, _ beg: Int, _ end: Int) {
898 let text = buffer.substring(beg, end)
899 var size = text.components(separatedBy: "\n").count - (text.hasSuffix("\n") ? 1 : 0)
900 if text.isEmpty { size = 0 }
901 guard size > 0 else { return }
902 if size < 10 {
903 buffer.point = beg
904 for _ in 0..<size {
905 buffer.point = buffer.lineStart(buffer.point)
906 buffer.insert(": ")
907 buffer.forwardLine(1)
908 }
909 return
910 }
911 let endMarker = buffer.marker(end)
912 defer { buffer.release(endMarker) }
913 buffer.point = beg
914 buffer.insert("#+begin_example\n")
915 escapeCode(buffer, from: buffer.point, to: endMarker.position)
916 buffer.point = endMarker.position
917 if !buffer.bolp { buffer.insert("\n") }
918 buffer.insert("#+end_example")
919 if !buffer.eolp { buffer.insert("\n") }
920 }
921}
922
923/// C-c C-c in a src block: `org-babel-execute-src-block`.
924public struct ExecuteSrcBlock: OrgCommand {
925 public init() {}
926 public var id: String { "org.babel.execute" }
927 public var title: String { "Run Source Block" }
928 public func applies(in context: EditContext) -> Bool { KeyContext.holds("src", in: context) }
929 public func run(in context: EditContext) -> CommandStep {
930 switch Babel.plan(context) {
931 case .success(let job): return .external(.babel(job))
932 case .failure(.message(let message)): return .failed(message)
933 }
934 }
935}