Sources/OrgCore/Compute/Babel.swift
1978 lines · 104234 bytes
102 symbols in this file
BabelJobRunnerNeedFinishKindBabelOutputBabelOptionsplanresumereplanplanlocatedcallAtinlineCallAtcallPropertiesexportsvaluesplitArgumentsinlineAtReferencesNeededplanplanBlockFailurenowebEvaluatesnowebTanglesexpandNowebexpandwrapinCommentedHeadingheadlineBodynormalizedBodyTablespickedlispNamescellTextdisassemblereassemblelispValuecellcacheStatecachedMessageexpandedBodycacheInputreadResultParamsevaluateLisppropertyLayersunescapesplitVarsparsevarValueVarValueCellnumberemacsFloatcellreferencedBlockreferencedBlocksresolveshellStringshellAssignmentsquotedpythonLiteralcelllispStringlispLiteralcellshiftRightemacsScriptevaluateNativelycellisProperisAtomValuefinishvaluerawValuerawResultfileResultimportTablecsvFieldspythonValuetextPythonLiteralNodeskipparseexistingResultsresultsHashresultsLineresultEndinsertInlineResultinsertResultwrapescapeCodeexamplifyExecuteSrcBlockappliesatCallOrInlinerun
1import CryptoKit
2import Foundation
3
4// Babel (ob-core.el, ob-shell.el, ob-python.el, ob-emacs-lisp.el, Org 9.8.7): executing a src
5// block is planned here, run by the platform, and finished here. The plan resolves header
6// arguments and refuses anything that would make the run differ from Emacs's; the finish turns
7// the output into a result the way `org-babel-insert-result` writes it.
8
9/// A src block run for the platform to carry out.
10public struct BabelJob: Sendable, Equatable {
11 public enum Runner: Sendable, Equatable {
12 /// An interpreter reading the program on standard input.
13 case process(executable: String, arguments: [String])
14 /// `emacs --batch` loading `input` as a script.
15 case emacs
16 /// A long-running interpreter named by `:session`: a shell gets `input` as commands, Python
17 /// runs `input` as a program in its globals.
18 case session(name: String, executable: String, python: Bool)
19 }
20
21 public let language: String
22 public let runner: Runner
23 /// Standard input, or the Emacs script. `Babel.resultFileToken` stands for a file the runner
24 /// creates; its contents come back as `BabelOutput.resultFile`.
25 public let input: String
26 /// `:dir`, relative to the file's folder; nil for the folder itself.
27 public let directory: String?
28 /// The block as it was planned, to find it again when the run ends.
29 public let block: Range<Int>
30 public let blockText: String
31 /// Asks every time (`:eval query`) rather than trusting by content.
32 public let alwaysAsk: Bool
33 public let finish: Finish
34 /// A named block a `:var` refers to: run it first, then `Babel.resume` with its output.
35 public var needs: [Need] = []
36 /// With `needs`: the text of every block the run may execute, the one asked about and
37 /// those its references reach. A later job for a block not among them is refused.
38 public internal(set) var approved: [String] = []
39 /// Values of the references resolved so far, and how to plan again.
40 var resolved: [String: Babel.VarValue] = [:]
41 var caret = 0
42 var options = Babel.Options()
43 /// A script for the runner to write to an executable file, for `Babel.scriptFileToken`.
44 public internal(set) var script: String?
45 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`.
46 public internal(set) var lispForm: String?
47 /// A Lisp header value couldn't be evaluated without the file's path: plan again with
48 /// `Babel.replan` before running.
49 public internal(set) var needsFile = false
50
51 public struct Need: Sendable, Equatable {
52 /// The `:var` value, `name` or `name(args)`.
53 public let reference: String
54 public let job: BabelJob
55 }
56
57 public struct Finish: Sendable, Equatable {
58 enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp }
59 let kind: Kind
60 var language = ""
61 public internal(set) var params: [String]
62 let output: Bool
63 let wrap: String?
64 let name: String?
65 /// An inline block: the result goes after it, in `{{{results(…)}}}`.
66 var inline = false
67 /// `:file` (under `:output-dir`) with `:results file`, and `:file-desc`.
68 var file: String?
69 var fileDescription: String?
70 /// The program writes `file` itself, as graphics languages do.
71 var writesFile = false
72 /// `:cache yes`: the hash for the `#+RESULTS[…]:` line.
73 var hash: String?
74 /// Column and row names a table `:var` gave up, put back on a table result
75 /// (`org-babel-reassemble-table`).
76 var colnames: [String]?
77 var rownames: [String]?
78 }
79}
80
81public struct BabelOutput: Sendable, Equatable {
82 public var stdout: String
83 public var resultFile: String?
84 /// What the program wrote to standard error, and its exit status: Emacs shows them in
85 /// `*Org-Babel Error Output*` and still inserts the results.
86 public var stderr: String
87 public var status: Int32
88 public init(stdout: String, resultFile: String? = nil, stderr: String = "", status: Int32 = 0) {
89 self.stdout = stdout
90 self.resultFile = resultFile
91 self.stderr = stderr
92 self.status = status
93 }
94
95 /// A line for the message area when the program failed or complained, else nil.
96 public var problem: String? {
97 let first = stderr.split(separator: "\n", omittingEmptySubsequences: true).first.map(String.init)
98 switch (status, first) {
99 case (0, nil): return nil
100 case (0, let line?): return line
101 case (let code, nil): return "Exited with status \(code)"
102 case (let code, let line?): return "Exited with status \(code): \(line)"
103 }
104 }
105}
106
107public enum Babel {
108 public static let resultFileToken = "__ORGSTAR_RESULT_FILE__"
109 /// Stands, in a job's arguments, for the executable file the runner writes `job.script` to.
110 public static let scriptFileToken = "__ORGSTAR_SCRIPT_FILE__"
111 static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"]
112 static let generic: [String: (String, [String])] = [
113 "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]),
114 "awk": ("awk", ["-f", "/dev/stdin"]),
115 ]
116
117 public struct Options: Sendable, Equatable {
118 public var python = "python3"
119 /// `shell-file-name`, from `SHELL` as Emacs sets it: runs blocks in the `shell` language
120 /// and chooses how variables are written in the `:cache` hash.
121 public var shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/sh"
122 /// The file the block is in, for Lisp header values that use it.
123 public var file: String?
124 public init() {}
125 }
126
127 // MARK: - Planning
128
129 /// The run for the src block at the caret, or why it can't run.
130 public static func plan(_ context: EditContext, options: Options = Options()) -> Result<BabelJob, Failure> {
131 plan(context, options: options, resolved: [:])
132 }
133
134 /// Plans the block again with the value of the block `job` needed: the output of
135 /// `need.job`, as `org-babel-ref-resolve` reads it.
136 public static func resume(_ job: BabelJob, need: BabelJob.Need, output: BabelOutput, text: String, tree: OrgTree) -> Result<BabelJob, Failure> {
137 var resolved = job.resolved
138 resolved[need.reference] = varValue(need.job.finish, output)
139 let context = EditContext(revision: 0, text: text, tree: tree, selection: [job.caret..<job.caret])
140 return plan(context, options: job.options, resolved: resolved)
141 }
142
143 /// Plans `job`'s block again knowing the file it is in.
144 public static func replan(_ job: BabelJob, file: String, text: String, tree: OrgTree) -> Result<BabelJob, Failure> {
145 var options = job.options
146 options.file = file
147 let context = EditContext(revision: 0, text: text, tree: tree, selection: [job.caret..<job.caret])
148 return plan(context, options: options, resolved: job.resolved)
149 }
150
151 static func plan(_ context: EditContext, options: Options, resolved: [String: VarValue]) -> Result<BabelJob, Failure> {
152 let text = context.text
153 let ns = text as NSString
154 let model = DocumentModel(tree: context.tree)
155 let refs = References(resolved: resolved, options: options)
156 func located(_ result: Result<BabelJob, Failure>) -> Result<BabelJob, Failure> {
157 result.map { job in
158 var job = job
159 (job.caret, job.resolved, job.options) = (context.caret, resolved, options)
160 return job
161 }
162 }
163 do {
164 if let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) {
165 return located(plan(block, params: try Params(block: block, model: model, text: ns), model: model, text: ns, options: options,
166 location: block.range, name: block.name, refs: refs))
167 }
168 if let call = try callAt(context.caret, model: model, text: ns) {
169 return located(plan(call.block, params: call.params, model: model, text: ns, options: options, location: call.location, name: call.name, refs: refs))
170 }
171 if let call = try inlineCallAt(context.caret, tree: context.tree, model: model, text: ns) {
172 return located(plan(call.block, params: call.params, model: model, text: ns, options: options, location: call.location, name: nil, inline: true, refs: refs))
173 }
174 if let inline = try inlineAt(context.caret, tree: context.tree, model: model, text: ns) {
175 return located(plan(inline.block, params: inline.params, model: model, text: ns, options: options, location: inline.block.range, name: nil, inline: true, refs: refs))
176 }
177 } catch let error as Failure {
178 return .failure(error)
179 } catch {
180 return .failure(.message("\(error)"))
181 }
182 return .failure(.message("Not in a source block"))
183 }
184
185 /// The `#+CALL:` line at `offset`: the block it names, with the call's inside header, its
186 /// arguments as `:var` and its end header over the block's own (`org-babel-lob-get-info`).
187 static func callAt(_ offset: Int, model: DocumentModel, text: NSString) throws -> (block: SrcBlockInfo, params: Params, location: Range<Int>, name: String?)? {
188 let lineRange = text.lineRange(for: NSRange(location: min(offset, text.length), length: 0))
189 let line = text.substring(with: lineRange)
190 guard let m = line.firstMatch(of: #/(?i)^[ \t]*#\+CALL:[ \t]*([^\s(\[]+)(?:\[([^\]]*)\])?\((.*?)\)[ \t]*(.*?)[ \t]*\n?$/#) else { return nil }
191 let name = String(m.1)
192 guard let block = model.srcBlocks.first(where: { $0.name == name }) else { throw Failure.message("Reference '\(name)' not found in this buffer") }
193 var extra = [lobDefaults] + callProperties(block, at: lineRange.location, model: model)
194 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
195 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
196 if !m.4.isEmpty { extra.append(Params.parse(String(m.4))) }
197 // The call's own #+NAME names its results.
198 var callName: String?
199 if lineRange.location > 0 {
200 let previous = text.substring(with: text.lineRange(for: NSRange(location: lineRange.location - 1, length: 0)))
201 callName = previous.firstMatch(of: #/(?i)^[ \t]*#\+NAME:[ \t]*(\S+)/#).map { String($0.1) }
202 }
203 let location = lineRange.location..<NSMaxRange(lineRange)
204 return (block, try Params(block: block, model: model, text: text, extra: extra), location, callName)
205 }
206
207 /// The inline `call_NAME[INSIDE](ARGS)[END]` at `offset`: like `#+CALL`, with the result
208 /// after the call.
209 static func inlineCallAt(_ offset: Int, tree: OrgTree, model: DocumentModel, text: NSString) throws -> (block: SrcBlockInfo, params: Params, location: Range<Int>)? {
210 var node = tree.root
211 var found: SyntaxNode?
212 while let child = node.child(containing: offset) {
213 if child.kind == .inlineBabelCall { found = child }
214 node = child
215 }
216 guard let object = found, let m = object.text.firstMatch(of: /^call_([^()\[\]\s]+)(?:\[([^\]]*)\])?\((.*?)\)(?:\[([^\]]*)\])?$/) else { return nil }
217 let name = String(m.1)
218 guard let block = model.srcBlocks.first(where: { $0.name == name }) else { throw Failure.message("Reference '\(name)' not found in this buffer") }
219 var extra = [lobDefaults] + callProperties(block, at: object.range.lowerBound, model: model)
220 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
221 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
222 if let end = m.4 { extra.append(Params.parse(String(end))) }
223 return (block, try Params(block: block, model: model, text: text, extra: extra), object.range)
224 }
225
226 /// The `header-args` properties where a call is, in the called block's language, which
227 /// `org-babel-lob-get-info` puts after `lobDefaults`.
228 static func callProperties(_ block: SrcBlockInfo, at location: Int, model: DocumentModel) -> [[(String, String)]] {
229 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= location }
230 return Params.propertyLayers(language: block.language ?? "", heading: heading, model: model)
231 }
232
233 /// `org-babel-default-lob-header-args`, over the called block's own header arguments.
234 static let lobDefaults = [(":exports", "results")]
235
236 /// `:exports` from the language's `org-babel-default-header-args:LANG` in Org 9.8.7.
237 static let languageExports = ["dot": "results", "plantuml": "results", "ditaa": "results", "gnuplot": "results",
238 "latex": "results", "lilypond": "results"]
239
240 /// What export shows of the src block, `#+CALL:` line, inline call or inline src block
241 /// starting at `offset` (`org-babel-exp-process-buffer`): `code`, `results`, `both` or
242 /// `none`, from the header arguments as evaluation resolves them. Calls never show code.
243 /// Nil when nothing there runs.
244 public static func exports(at offset: Int, tree: OrgTree, model: DocumentModel, text: NSString) -> String? {
245 func value(_ params: Params?, language: String?) -> String {
246 (params?.single[":exports"] ?? language.flatMap { languageExports[$0] } ?? "code").lowercased()
247 }
248 if let block = model.srcBlocks.first(where: { $0.range.lowerBound == offset }) {
249 return value(try? Params(block: block, model: model, text: text), language: block.language)
250 }
251 let line = text.substring(with: text.lineRange(for: NSRange(location: min(offset, text.length), length: 0)))
252 let call: Params?
253 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil {
254 call = (try? callAt(offset, model: model, text: text))?.params
255 } else if tree.root.descendants().contains(where: { $0.kind == .inlineBabelCall && $0.range.lowerBound == offset }) {
256 // `org-babel-remove-result` only takes away `#+RESULTS:`, so an inline call's
257 // `{{{results(...)}}}` stays whatever `:exports` says.
258 return "results"
259 } else if let inline = try? inlineAt(offset, tree: tree, model: model, text: text), inline.block.range.lowerBound == offset {
260 return value(inline.params, language: inline.block.language)
261 } else {
262 return nil
263 }
264 // `org-babel-exp-code` for a call is `org-babel-exp-call-line-template`, empty.
265 switch call?.single[":exports"]?.lowercased() ?? "results" {
266 case "both", "results": return "results"
267 default: return "none"
268 }
269 }
270
271 /// Call arguments split on commas outside quotes and parentheses.
272 static func splitArguments(_ s: String) -> [String] {
273 var parts: [String] = []
274 var current = ""
275 var depth = 0
276 var quoted = false
277 for c in s {
278 if c == "\"" { quoted.toggle() }
279 if !quoted, c == "(" { depth += 1 }
280 if !quoted, c == ")" { depth -= 1 }
281 if c == ",", !quoted, depth == 0 {
282 parts.append(current)
283 current = ""
284 } else {
285 current.append(c)
286 }
287 }
288 parts.append(current)
289 return parts.map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
290 }
291
292 /// The inline `src_LANG[HEADERS]{BODY}` at `offset`.
293 static func inlineAt(_ offset: Int, tree: OrgTree, model: DocumentModel, text: NSString) throws -> (block: SrcBlockInfo, params: Params)? {
294 var node = tree.root
295 var found: SyntaxNode?
296 while let child = node.child(containing: offset) {
297 if child.kind == .inlineSourceBlock { found = child }
298 node = child
299 }
300 guard let object = found, let m = object.text.firstMatch(of: /^src_([^\s\[{]+)(?:\[([^\]]*)\])?\{(.*)\}$/) else { return nil }
301 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= object.range.lowerBound }
302 let block = SrcBlockInfo(range: object.range, heading: heading, name: nil, language: String(m.1), switches: [], body: String(m.3), headerArguments: [:])
303 let extra = m.2.map { [Params.parse(String($0))] } ?? []
304 return (block, try Params(block: block, model: model, text: text, defaults: [(":exports", "results"), (":hlines", "yes")], extra: extra, inline: true))
305 }
306
307 /// What `:var` references resolve to while planning.
308 struct References {
309 var resolved: [String: VarValue]
310 var options: Options
311 /// Blocks being planned for a reference, to stop cycles.
312 var visiting: Set<String> = []
313 }
314
315 /// A reference to a block that has to run first.
316 struct Needed: Error {
317 let need: BabelJob.Need
318 }
319
320 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
321 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> {
322 var evaluated = params
323 evaluated.evaluateLisp(file: options.file)
324 let needsFile = options.file == nil && evaluated.lisp.contains { evaluated.evaluated[$0] == nil }
325 return planBlock(block, params: params, model: model, text: ns, options: options, location: location, name: name, inline: inline,
326 refs: refs, useCache: useCache).map { job in
327 var job = job
328 job.needsFile = needsFile
329 return job
330 }
331 }
332
333 static func planBlock(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
334 location: Range<Int>, name: String?, inline: Bool, refs: References?, useCache: Bool) -> Result<BabelJob, Failure> {
335 let refs = refs ?? References(resolved: [:], options: options)
336 var params = params
337 params.evaluateLisp(file: options.file)
338 let needsFile = options.file == nil && params.lisp.contains { params.evaluated[$0] == nil }
339 guard let language = block.language else { return .failure(.message("This block has no language")) }
340 let eval = params.single[":eval"]?.lowercased()
341 if eval == "never" || eval == "no" {
342 return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled."))
343 }
344 if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language), !shells.contains(language) {
345 return .failure(.message(":cmdline isn't supported for \(language) yet; nothing was run."))
346 }
347 for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil
348 && !(shells.contains(language) && [":stdin", ":shebang"].contains(key)) {
349 return .failure(.message("\(key) isn't supported yet; nothing was run."))
350 }
351 let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) }
352 if session != nil, !shells.contains(language), language != "python" {
353 return .failure(.message(":session isn't supported for \(language) yet; nothing was run."))
354 }
355 var body = normalizedBody(block)
356 if nowebEvaluates(params.single[":noweb"]) {
357 do {
358 body = try expandNoweb(block, params: params, model: model, text: ns)
359 } catch let error as Failure {
360 return .failure(error)
361 } catch {
362 return .failure(.message("\(error)"))
363 }
364 }
365 let output = params.results.contains("output")
366 let file = params.single[":file"].map { name in
367 params.single[":output-dir"].map { $0.hasSuffix("/") ? $0 + name : $0 + "/" + name } ?? name
368 }
369 let toFile = params.results.contains("file")
370 var hash: String?
371 var tables = Tables()
372 let finishBase = { (kind: BabelJob.Finish.Kind) in
373 BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline,
374 file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash,
375 colnames: tables.picked(tables.colnames, ":colnames", params), rownames: tables.picked(tables.rownames, ":rownames", params))
376 }
377 let vars: [(String, VarValue)]
378 var stdin: VarValue?
379 do {
380 tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) }, params: params)
381 vars = tables.vars
382 stdin = try params.single[":stdin"].map { try resolve($0, model: model, text: ns, refs: refs) }
383 } catch let needed as Needed {
384 // Asked about once, with every block the references reach; asked every time when
385 // any of them has `:eval query`.
386 let reached = referencedBlocks(params, model: model, text: ns, seen: [])
387 let texts = [ns.substring(with: NSRange(location))] + reached.map { ns.substring(with: NSRange($0.range)) }
388 let query = eval == "query" || reached.contains { (try? Params(block: $0, model: model, text: ns))?.single[":eval"]?.lowercased() == "query" }
389 var job = BabelJob(
390 language: language, runner: .process(executable: "", arguments: []), input: "", directory: params.single[":dir"],
391 block: location, blockText: texts.joined(separator: "\n"), alwaysAsk: query, finish: finishBase(.generic), needs: [needed.need]
392 )
393 job.approved = texts
394 return .success(job)
395 } catch let error as Failure {
396 return .failure(error)
397 } catch {
398 return .failure(.message("\(error)"))
399 }
400 if useCache, !inline, !needsFile, params.single[":cache"] == "yes" {
401 do {
402 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name,
403 call: location != block.range, text: ns, options: options)
404 if state.current { return .failure(.message(cachedMessage(state.value))) }
405 // Appended or prepended results accumulate, so they carry no hash.
406 if params.results.contains("replace") { hash = state.hash }
407 } catch let error as Failure {
408 return .failure(error)
409 } catch {
410 return .failure(.message("\(error)"))
411 }
412 }
413 let blockText = ns.substring(with: NSRange(location))
414 let dir = params.single[":dir"]
415 let alwaysAsk = eval == "query"
416
417 if shells.contains(language) {
418 let valueIsExitStatus = params.results.contains("value")
419 let executable = language == "shell" ? options.shell : language
420 let lines = shellAssignments(vars, shell: executable, params: params)
421 let cmdline = params.single[":cmdline"]
422 let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil }
423 if stdin != nil || cmdline != nil || (session == nil && shebang != nil) {
424 // `org-babel-sh-evaluate`: an executable script, run by the shell with the
425 // command line after it and `:stdin` as its input; the result is its standard
426 // output, or its exit status for `:results value`. `:stdin` and `:cmdline`
427 // run it even with a session, which the script doesn't use.
428 var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n"
429 if params.single[":padline"] != "no" { script += "\n" }
430 script += (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
431 let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "")
432 var job = BabelJob(
433 language: language, runner: .process(executable: executable, arguments: ["-c", command]),
434 input: stdin.map { shellString($0) } ?? "", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk,
435 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
436 )
437 job.script = script
438 return .success(job)
439 }
440 let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
441 return .success(BabelJob(
442 language: language, runner: session.map { .session(name: $0, executable: executable, python: false) } ?? .process(executable: executable, arguments: []),
443 input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir,
444 block: location, blockText: blockText, alwaysAsk: alwaysAsk,
445 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
446 ))
447 }
448 if language == "python", let session {
449 // `org-babel-python--eval-ast`: the body at top level in the session, the value its
450 // last expression's.
451 let lines = vars.map { "\($0.0)=\(pythonLiteral($0.1))" }
452 let source = (lines + [body]).joined(separator: "\n")
453 let literal = String(data: try! JSONSerialization.data(withJSONObject: [source]), encoding: .utf8)!.dropFirst().dropLast()
454 let program: String
455 if output {
456 program = "exec(compile(\(literal), '<org-babel>', 'exec'), globals())\n"
457 } else {
458 program = pythonFormatter + """
459 import ast as __orgstar_ast
460 __orgstar_tree = __orgstar_ast.parse(\(literal))
461 __orgstar_last = None
462 if __orgstar_tree.body and isinstance(__orgstar_tree.body[-1], __orgstar_ast.Expr):
463 __orgstar_last = __orgstar_ast.Expression(__orgstar_tree.body.pop().value)
464 exec(compile(__orgstar_tree, '<org-babel>', 'exec'), globals())
465 __orgstar_format(eval(compile(__orgstar_last, '<org-babel>', 'eval'), globals()) if __orgstar_last else None, '\(resultFileToken)', [
466 """ + params.results.map { "\"\($0)\"" }.joined(separator: ", ") + "])\n"
467 }
468 return .success(BabelJob(
469 language: language, runner: .session(name: session, executable: params.single[":python"] ?? options.python, python: true),
470 input: program, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.python)
471 ))
472 }
473 if language == "python" {
474 let lines = vars.map { "\($0.0)=\(pythonLiteral($0.1))" }
475 var full = (lines + [body]).joined(separator: "\n")
476 if let returnValue = params.single[":return"], !output { full += "\nreturn \(returnValue)" }
477 let input: String
478 if output {
479 input = full + "\n"
480 } else {
481 input = pythonFormatter + "def main():\n" + shiftRight(full) + "\n\n__orgstar_format(main(), '\(resultFileToken)', ["
482 + params.results.map { "\"\($0)\"" }.joined(separator: ", ") + "])\n"
483 }
484 return .success(BabelJob(
485 language: language, runner: .process(executable: params.single[":python"] ?? options.python, arguments: []),
486 input: input, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk,
487 finish: finishBase(.python)
488 ))
489 }
490 if language == "emacs-lisp" || language == "elisp" {
491 let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" }
492 let expanded = bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n ")))\n\(body)\n)"
493 let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)"
494 var job = BabelJob(
495 language: language, runner: .emacs, input: emacsScript(form: form, params: params.results),
496 directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp)
497 )
498 job.lispForm = form
499 return .success(job)
500 }
501 if ["dot", "plantuml", "mermaid"].contains(language) {
502 // ob-dot, ob-plantuml and ob-mermaid: the program draws into `:file`.
503 guard let file else { return .failure(.message("\(language) code blocks need a :file header argument")) }
504 let quoted = "'" + file.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
505 let ext = (file as NSString).pathExtension.isEmpty ? "png" : (file as NSString).pathExtension
506 let cmdline = params.single[":cmdline"].map { " " + $0 } ?? ""
507 var input = body + "\n"
508 let command: String
509 switch language {
510 case "dot":
511 command = "dot\(cmdline) -T\(ext) -o \(quoted)"
512 case "plantuml":
513 if body.range(of: "^[ \\t]*@start", options: .regularExpression) == nil { input = "@startuml\n" + body + "\n@enduml\n" }
514 command = "plantuml\(cmdline) -p -t\(ext) > \(quoted)"
515 default:
516 command = "f=$(mktemp).mmd; cat > \"$f\"; mmdc\(cmdline) -i \"$f\" -o \(quoted); s=$?; rm -f \"$f\"; exit $s"
517 }
518 var finish = finishBase(.generic)
519 finish.file = file
520 finish.writesFile = true
521 finish.params = params.results.filter { !["table", "scalar", "verbatim", "list", "vector"].contains($0) } + (params.results.contains("file") ? [] : ["file"])
522 return .success(BabelJob(
523 language: language, runner: .process(executable: "/bin/sh", arguments: ["-c", command]),
524 input: input, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finish
525 ))
526 }
527 if let (executable, arguments) = generic[language] {
528 guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) }
529 return .success(BabelJob(
530 language: language, runner: .process(executable: params.single[":cmd"] ?? executable, arguments: arguments),
531 input: body + "\n", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk,
532 finish: BabelJob.Finish(kind: .generic, language: language, params: params.results + (output ? [] : ["output"]), output: true, wrap: params.wrap, name: name, inline: inline, hash: hash)
533 ))
534 }
535 return .failure(.message("No way to run \(language) blocks yet."))
536 }
537
538 public enum Failure: Error, Equatable {
539 case message(String)
540 }
541
542 // MARK: - Noweb
543
544 /// `org-babel-noweb-p` in the `:eval` context.
545 static func nowebEvaluates(_ value: String?) -> Bool {
546 (value ?? "").split(whereSeparator: \.isWhitespace).contains { ["yes", "no-export", "strip-export", "eval", "strip-tangle"].contains(String($0)) }
547 }
548
549 /// `org-babel-noweb-p` for `:tangle`.
550 static func nowebTangles(_ value: String?) -> Bool {
551 (value ?? "").split(whereSeparator: \.isWhitespace).contains { ["yes", "tangle", "no-export", "strip-export", "strip-tangle"].contains(String($0)) }
552 }
553
554 /// `org-babel-expand-noweb-references` for evaluation: each `<<name>>` becomes the body of
555 /// the block named so, or of every block with that `:noweb-ref`, joined by `:noweb-sep`, or
556 /// the contents of the heading with that ID; the text before it on its line goes before
557 /// each line it brings.
558 /// `tangling` expands the blocks it brings in as tangling would (`:noweb tangle`), not as
559 /// evaluation does.
560 /// `links` gives a block's begin and end comments, for `:comments noweb` when tangling.
561 static func expandNoweb(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text: NSString, tangling: Bool = false, depth: Int = 0,
562 links: ((SrcBlockInfo, Params) throws -> (begin: String, end: String))? = nil) throws -> String {
563 guard depth < 32 else { throw Failure.message("Noweb references nest too deeply") }
564 let body = normalizedBody(block)
565 let prefixing = params.single[":noweb-prefix"].map { $0 != "no" && $0 != "nil" } ?? true
566 let commenting = tangling && links != nil && params.single[":comments"] == "noweb"
567 func expand(_ other: SrcBlockInfo) throws -> String {
568 let otherParams = try Params(block: other, model: model, text: text)
569 let expands = tangling ? nowebTangles(otherParams.single[":noweb"]) : nowebEvaluates(otherParams.single[":noweb"])
570 let b = expands
571 ? try expandNoweb(other, params: otherParams, model: model, text: text, tangling: tangling, depth: depth + 1, links: links)
572 : normalizedBody(other)
573 guard commenting, let links else { return b }
574 // `c-wrap`: commented in this block's language, trimmed.
575 func wrap(_ s: String) throws -> String {
576 try Tangle.commentRegion(s, language: block.language ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
577 }
578 var (begin, end) = try links(other.name == nil ? block : other, other.name == nil ? params : otherParams)
579 // A `:noweb-ref` block without a name: the link is the including block's, the name empty.
580 if other.name == nil {
581 if let split = begin.range(of: "][") { begin = begin[..<split.lowerBound] + "][]]" }
582 end = " ends here"
583 }
584 return try wrap(begin) + "\n" + b + "\n" + (try wrap(end)) + "\n"
585 }
586 func inCommentedHeading(_ b: SrcBlockInfo) -> Bool {
587 guard let h = b.heading else { return false }
588 return ([h] + model.ancestors(of: h)).contains { model.headings[$0].title == "COMMENT" || model.headings[$0].title.hasPrefix("COMMENT ") }
589 }
590 let regex = try! NSRegularExpression(pattern: "(.*?)(<<([^ \\t\\n](?:.*?[^ \\t\\n])?)>>)")
591 let ns = body as NSString
592 var out = ""
593 var at = 0
594 for m in regex.matches(in: body, range: NSRange(location: 0, length: ns.length)) {
595 let prefix = ns.substring(with: m.range(at: 1))
596 let id = ns.substring(with: m.range(at: 3))
597 out += ns.substring(with: NSRange(location: at, length: m.range(at: 2).location - at))
598 at = NSMaxRange(m.range)
599 var expansion = ""
600 if id.range(of: "\\(.*\\)", options: .regularExpression) != nil {
601 throw Failure.message("Noweb references that run a block (\(id)) aren't supported yet; nothing was run.")
602 } else if let heading = model.headings.firstIndex(where: { h in
603 h.properties.contains { ($0.key.uppercased() == "ID" && $0.value == id) || ($0.key.uppercased() == "CUSTOM_ID" && "#" + $0.value == id) }
604 }) {
605 expansion = headlineBody(heading, model: model, text: text)
606 } else if let named = model.srcBlocks.first(where: { $0.name == id }), !inCommentedHeading(named) {
607 expansion = try expand(named)
608 } else {
609 var parts: [String] = []
610 var separators: [String] = []
611 for other in model.srcBlocks where !inCommentedHeading(other) {
612 let p = try Params(block: other, model: model, text: text)
613 guard p.single[":noweb-ref"] == id else { continue }
614 parts.append(try expand(other))
615 separators.append(p.single[":noweb-sep"] ?? "\n")
616 }
617 for (i, part) in parts.enumerated() {
618 if i > 0 { expansion += separators[i - 1] }
619 expansion += part
620 }
621 }
622 out += prefixing ? expansion.components(separatedBy: CharacterSet(charactersIn: "\n\r")).joined(separator: "\n" + prefix) : expansion
623 }
624 return out + ns.substring(from: at)
625 }
626
627 /// `org-babel-ref-headline-body`: the entry after its planning and drawers.
628 static func headlineBody(_ index: Int, model: DocumentModel, text: NSString) -> String {
629 let heading = model.headings[index]
630 let buffer = EmacsBuffer(text as String, point: heading.headingRange.lowerBound)
631 buffer.endOfMetaData()
632 let start = buffer.point
633 let end = model.headings[(index + 1)...].first.map { $0.headingRange.lowerBound } ?? text.length
634 return start < end ? text.substring(with: NSRange(location: start, length: end - start)).trimmingCharacters(in: .whitespacesAndNewlines) : ""
635 }
636
637 /// `org-babel--normalize-body`: without the final newline, and without the indentation
638 /// common to its lines unless `-i` keeps it.
639 static func normalizedBody(_ block: SrcBlockInfo) -> String {
640 var lines = block.body.components(separatedBy: "\n")
641 if lines.last == "" { lines.removeLast() }
642 // Org escapes lines starting with `*` or `#+` inside blocks with a comma.
643 lines = lines.map { $0.replacingOccurrences(of: "^([ \\t]*),([,*]|#\\+)", with: "$1$2", options: .regularExpression) }
644 if !block.switches.contains("-i") {
645 let indents = lines.filter { $0.contains(where: { !$0.isWhitespace }) }.map { $0.prefix { $0 == " " || $0 == "\t" }.count }
646 let common = indents.min() ?? 0
647 lines = lines.map { String($0.dropFirst(min(common, $0.prefix { $0 == " " || $0 == "\t" }.count))) }
648 }
649 return lines.joined(separator: "\n")
650 }
651
652 // MARK: - Tables in variables
653
654 /// Variables after `org-babel-disassemble-tables`, with the names taken off their tables.
655 struct Tables {
656 var vars: [(String, VarValue)] = []
657 var colnames: [(name: String, names: [Cell])] = []
658 var rownames: [(name: String, names: [Cell])] = []
659
660 /// `org-babel-pick-name`: a Lisp list given as the header argument, or the last
661 /// variable's names, unless the header argument is absent, whose `nil` selects none.
662 func picked(_ names: [(name: String, names: [Cell])], _ key: String, _ params: Params) -> [String]? {
663 if let list = params.evaluated[key].flatMap(Babel.lispNames) { return list }
664 guard params.single[key] != nil, let last = names.last else { return nil }
665 return last.names.map(Babel.cellText)
666 }
667 }
668
669 /// A Lisp list of strings and numbers, as names.
670 static func lispNames(_ value: Sexp) -> [String]? {
671 guard case .list(let items) = value else { return nil }
672 return try? items.map { item in
673 if let s = item.string { return s }
674 let printed = Elisp.printed(item)
675 guard number(printed) == printed else { throw Failure.message(printed) }
676 return printed
677 }
678 }
679
680 static func cellText(_ c: Cell) -> String {
681 switch c {
682 case .number(let n): n
683 case .text(let s): s
684 }
685 }
686
687 /// `org-babel-disassemble-tables`: a table's header row comes off when `:colnames` asks, or
688 /// by default when only its second row is a rule; its first column with `:rownames`; its
689 /// rules unless `:hlines yes`.
690 static func disassemble(_ vars: [(String, VarValue)], params: Params) throws -> Tables {
691 for key in [":colnames", ":rownames"] where params.lisp.contains(key) && params.evaluated[key].flatMap(lispNames) == nil {
692 throw Failure.message("\(key) \(params.single[key] ?? "") is Lisp that only Emacs can evaluate; nothing was run.")
693 }
694 let colnames = params.single[":colnames"]
695 let rownames = params.single[":rownames"]
696 let hlines = params.single[":hlines"] ?? "no"
697 var tables = Tables()
698 for (name, value) in vars {
699 guard case .table(var rows) = value else {
700 tables.vars.append((name, value))
701 continue
702 }
703 let auto = rows.count > 1 && rows[0] != nil && rows[1] == nil && !rows.dropFirst(2).contains(where: { $0 == nil })
704 if colnames != "no", colnames != nil || auto {
705 while let first = rows.first, first == nil { rows.removeFirst() }
706 if let header = rows.first ?? nil {
707 rows.removeFirst(rows.count > 1 && rows[1] == nil ? 2 : 1)
708 tables.colnames.append((name, header))
709 }
710 }
711 if let rownames, rownames != "no" {
712 let kept = rows.compactMap { $0 }
713 tables.rownames.append((name, kept.map { $0.first ?? .text("") }))
714 rows = kept.map { Array($0.dropFirst()) }
715 }
716 if hlines != "yes" { rows = rows.filter { $0 != nil } }
717 tables.vars.append((name, .table(rows)))
718 }
719 return tables
720 }
721
722 /// `org-babel-reassemble-table` on a result.
723 static func reassemble(_ value: Value, _ finish: BabelJob.Finish) -> Value {
724 guard case .table(var rows) = value else { return value }
725 if let names = finish.rownames, names.count == rows.count {
726 var next = names.makeIterator()
727 rows = rows.map { row in row.map { [next.next() ?? ""] + $0 } }
728 }
729 if let names = finish.colnames, let first = rows.first ?? nil, first.count == names.count {
730 rows = [names, nil] + rows
731 }
732 return .table(rows)
733 }
734
735 /// A `:var` value that is Lisp (`org-babel-read`): evaluated, as a number, string, list or
736 /// table.
737 static func lispValue(_ value: String) throws -> VarValue {
738 let refusal = Failure.message(":var value \(value) is Lisp that only Emacs can evaluate.")
739 guard let form = try? LispReader.readFirst(value).sexp, let result = try? Elisp().eval(form) else { throw refusal }
740 func cell(_ v: Sexp) throws -> Cell {
741 if let s = v.string { return .text(s) }
742 if let n = number(Elisp.printed(v)), Elisp.printed(v) == n { return .number(n) }
743 throw refusal
744 }
745 if let s = result.string { return .scalar(s) }
746 if case .list(let items) = result, !items.isEmpty {
747 if items.allSatisfy({ if case .list = $0 { return true }; return $0 == .symbol("hline") }) {
748 return .table(try items.map { item in
749 if item == .symbol("hline") { return nil }
750 guard case .list(let cells) = item else { throw refusal }
751 return try cells.map(cell)
752 })
753 }
754 return .list(try items.map(cell))
755 }
756 if case .number(let n) = try? cell(result) { return .number(n) }
757 throw refusal
758 }
759
760 // MARK: - Cache
761
762 /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results
763 /// already carry it, with their value when it can be read.
764 static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?,
765 call: Bool, text: NSString, options: Options) throws -> (hash: String, current: Bool, value: VarValue?) {
766 // The hash is taken outside the shell's own binding of `shell-file-name`, so its
767 // variables are written for the user's shell whatever the block's language.
768 let expanded = try languageExpansion(body, language: language, tables: tables, params: params)
769 ?? expandedBody(language: language, vars: tables.vars, body: body, shell: options.shell, params: params)
770 let input = try cacheInput(language: language, params: params, tables: tables, expanded: expanded, call: call)
771 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
772 guard let line = existingResults(name: name, block: location, text: text), resultsHash(at: line, text: text) == hash else {
773 return (hash, false, nil)
774 }
775 return (hash, true, readResult(after: line, text: text))
776 }
777
778 static func cachedMessage(_ value: VarValue?) -> String {
779 value.map { "Cached: " + lispLiteral($0) } ?? "Cached; nothing was run."
780 }
781
782 /// The body with its variables, as the language's `org-babel-expand-body:` writes it.
783 static func expandedBody(language: String, vars: [(String, VarValue)], body: String, shell: String, params: Params) -> String {
784 if shells.contains(language) {
785 return (shellAssignments(vars, shell: shell, params: params) + [body]).joined(separator: "\n")
786 }
787 if language == "python" {
788 return (vars.map { "\($0.0)=\(pythonLiteral($0.1))" } + [body]).joined(separator: "\n")
789 }
790 if language == "emacs-lisp" || language == "elisp" {
791 let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" }
792 return bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n ")))\n\(body)\n)"
793 }
794 return body
795 }
796
797 /// What `org-babel-sha1-hash` hashes: the header arguments after `org-babel-process-params`,
798 /// sorted by key and printed with `%S`, then the expanded body.
799 static func cacheInput(language: String, params: Params, tables: Tables, expanded: String, call: Bool) throws -> String {
800 if let key = params.lisp.first(where: { params.evaluated[$0] == nil }) {
801 throw Failure.message(":cache with \(key) \(params.single[key] ?? "") needs Emacs to evaluate the Lisp; nothing was run.")
802 }
803 let ignored: Set<String> = ["replace", "silent", "none", "discard", "append", "prepend"]
804 let drawsFile = language == "dot" || language == "plantuml"
805 // `org-babel-default-header-args`, the language's and `org-babel-default-lob-header-args`.
806 var defaults = [":session": "none", ":exports": call || drawsFile ? "results" : "code", ":cache": "no", ":noweb": "no", ":hlines": "no", ":tangle": "no"]
807 if language == "emacs-lisp" || language == "elisp" { defaults[":lexical"] = "no" }
808 var entries: [(key: String, printed: String)] = []
809 for (key, value) in defaults.merging(params.single, uniquingKeysWith: { $1 }) {
810 let printed: String
811 if let evaluated = params.evaluated[key] {
812 printed = Elisp.printed(evaluated, escape: true)
813 } else if value.isEmpty {
814 continue
815 } else if key == ":exports" {
816 printed = lispString(value.split(whereSeparator: \.isWhitespace).map(String.init).filter { !ignored.contains($0) }.sorted().joined(separator: " "))
817 } else if params.single[key] != nil, !params.quoted.contains(key), let n = number(value) {
818 printed = n
819 } else {
820 printed = lispString(value)
821 }
822 entries.append((key, printed))
823 }
824 var words = params.results
825 if drawsFile, !words.contains(where: { Params.resultGroups[0].contains($0) }) { words.append("file") }
826 let kept = words.filter { !ignored.contains($0) }.sorted()
827 if !kept.isEmpty { entries.append((":result-params", "(" + kept.map { lispString($0) }.joined(separator: " ") + ")")) }
828 entries.append((":result-type", words.contains("output") ? "output" : "value"))
829 entries.append((":results", lispString(kept.joined(separator: " "))))
830 if let wrap = params.wrap, !wrap.isEmpty { entries.append((":wrap", lispString(wrap))) }
831 for (key, names) in [(":colname-names", tables.colnames), (":rowname-names", tables.rownames)] where !names.isEmpty {
832 entries.append((key, "(" + names.map { "(\($0.name) " + lispLiteral(.list($0.names)).dropFirst() }.joined(separator: " ") + ")"))
833 }
834 for (name, value) in tables.vars {
835 switch value {
836 case .scalar, .number: entries.append((":var", "(\(name) . \(lispLiteral(value)))"))
837 case .list, .table: entries.append((":var", "(\(name) " + lispLiteral(value).dropFirst()))
838 }
839 }
840 let sorted = entries.enumerated().sorted { ($0.element.key, $0.offset) < ($1.element.key, $1.offset) }.map(\.element.printed)
841 return sorted.joined(separator: ":") + "-" + expanded
842 }
843
844 /// `org-babel-read-result` for fixed-width lines, a table or a plain list after the
845 /// `#+RESULTS` line at `line`; nil for anything else.
846 static func readResult(after line: Int, text ns: NSString) -> VarValue? {
847 var location = NSMaxRange(ns.lineRange(for: NSRange(location: line, length: 0)))
848 var lines: [String] = []
849 while location < ns.length {
850 let range = ns.lineRange(for: NSRange(location: location, length: 0))
851 let content = ns.substring(with: range).trimmingCharacters(in: .newlines)
852 if content.trimmingCharacters(in: .whitespaces).isEmpty { break }
853 lines.append(content.trimmingCharacters(in: .whitespaces))
854 location = NSMaxRange(range)
855 }
856 guard let first = lines.first else { return nil }
857 if first == ":" || first.hasPrefix(": ") {
858 var fixed: [String] = []
859 for l in lines {
860 guard l == ":" || l.hasPrefix(": ") else { break }
861 fixed.append(String(l.dropFirst(2)))
862 }
863 let value = fixed.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
864 return number(value).map(VarValue.number) ?? .scalar(value)
865 }
866 if first.hasPrefix("|") {
867 var rows: [[Cell]?] = []
868 for l in lines {
869 guard l.hasPrefix("|") else { break }
870 if l.hasPrefix("|-") {
871 rows.append(nil)
872 continue
873 }
874 let cells = l.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
875 rows.append((l.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell))
876 }
877 return .table(rows)
878 }
879 if first.firstMatch(of: #/^[-+*] /#) != nil {
880 var items: [Cell] = []
881 for l in lines {
882 guard let m = l.firstMatch(of: /^[-+*] (.*)$/) else { return nil }
883 items.append(cell(String(m.1)))
884 }
885 return .list(items)
886 }
887 return nil
888 }
889
890 // MARK: - Header arguments
891
892 struct Params {
893 var results: [String] = ["replace"]
894 var single: [String: String] = [:]
895 var vars: [(name: String, value: String)] = []
896 var wrap: String?
897 /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`, `'`, `` ` `` or `[`.
898 var lisp: Set<String> = []
899 /// Keys whose value was a quoted string, which `org-babel-read` doesn't take for a number.
900 var quoted: Set<String> = []
901 /// Lisp values evaluated to something other than a string or number, such as a list of
902 /// column names.
903 var evaluated: [String: Sexp] = [:]
904
905 /// `org-babel-read` on the Lisp values the interpreter can evaluate: a string or number
906 /// takes the form's place; other values are kept in `evaluated`.
907 mutating func evaluateLisp(file: String?) {
908 for key in lisp {
909 guard let form = try? LispReader.readFirst(single[key] ?? "").sexp,
910 let value = try? (file.map { Tangle.fileLisp(path: $0) } ?? Elisp()).eval(form) else { continue }
911 let printed = Elisp.printed(value)
912 if let s = value.string {
913 single[key] = s
914 quoted.insert(key)
915 lisp.remove(key)
916 } else if Babel.number(printed) == printed {
917 single[key] = printed
918 lisp.remove(key)
919 } else {
920 evaluated[key] = value
921 }
922 }
923 }
924
925 static let resultGroups: [Set<String>] = [
926 ["file", "list", "vector", "table", "scalar", "verbatim"],
927 ["raw", "html", "latex", "org", "code", "pp", "drawer", "link", "graphics"],
928 ["replace", "silent", "none", "discard", "append", "prepend"],
929 ["output", "value"],
930 ]
931
932 /// `defaults` go under the properties, as `org-babel-default-inline-header-args` do.
933 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [],
934 inline: Bool = false) throws {
935 var layers: [[(String, String)]] = [defaults]
936 layers += Params.propertyLayers(language: block.language ?? "", heading: block.heading, model: model)
937 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
938 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
939 var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
940 while let first = rest.split(separator: " ").first, first.hasPrefix("-") || first.hasPrefix("+") {
941 rest = String(rest.dropFirst(first.count)).trimmingCharacters(in: .whitespaces)
942 }
943 layers.append(Params.parse(rest))
944 }
945 // `#+HEADER:` lines above the block.
946 var headers: [String] = []
947 var location = block.range.lowerBound
948 while !inline, location > 0 {
949 let previous = text.lineRange(for: NSRange(location: location - 1, length: 0))
950 let line = text.substring(with: previous).trimmingCharacters(in: .whitespacesAndNewlines)
951 guard line.range(of: "(?i)^#\\+(header|name|caption|attr_[a-z]+|results|plot|label):", options: .regularExpression) != nil else { break }
952 if line.lowercased().hasPrefix("#+header:") { headers.insert(String(line.dropFirst("#+header:".count)), at: 0) }
953 location = previous.location
954 }
955 for header in headers { layers.append(Params.parse(header)) }
956 layers += extra
957
958 for layer in layers {
959 for (key, raw) in layer {
960 let quoted = key != ":var" && raw.count >= 2 && raw.hasPrefix("\"") && raw.hasSuffix("\"")
961 let value = quoted ? Params.unescape(String(raw.dropFirst().dropLast())) : raw
962 if key != ":var" {
963 if !quoted, let first = value.first, "('`[".contains(first) { lisp.insert(key) } else { lisp.remove(key) }
964 if quoted { self.quoted.insert(key) } else { self.quoted.remove(key) }
965 }
966 switch key {
967 case ":results":
968 for word in value.split(whereSeparator: \.isWhitespace).map(String.init) {
969 if let group = Params.resultGroups.first(where: { $0.contains(word) }) { results.removeAll { group.contains($0) } }
970 results.removeAll { $0 == word }
971 results.insert(word, at: 0)
972 }
973 case ":var":
974 for value in Params.splitVars(value) {
975 guard let eq = value.firstIndex(of: "=") else { throw Failure.message(":var \(value) needs a name and a value") }
976 let name = value[..<eq].trimmingCharacters(in: .whitespaces)
977 let assigned = value[value.index(after: eq)...].trimmingCharacters(in: .whitespaces)
978 vars.removeAll { $0.name == name }
979 vars.append((name, assigned))
980 }
981 case ":wrap":
982 wrap = value
983 default:
984 single[key] = value
985 }
986 }
987 }
988 }
989
990 /// `org-babel-params-from-properties` under `heading`, or before any: the inherited
991 /// `header-args`, then the inherited `header-args:LANG`, each the nearest value with `+`
992 /// additions after it.
993 static func propertyLayers(language: String, heading: Int?, model: DocumentModel) -> [[(String, String)]] {
994 var levels = [model.fileProperties]
995 if let heading {
996 levels += (model.ancestors(of: heading).reversed() + [heading]).map { model.headings[$0].properties }
997 }
998 return ["header-args", "header-args:\(language)"].compactMap { key in
999 var value: String?
1000 for entries in levels {
1001 for entry in entries where entry.key.lowercased() == key {
1002 value = entry.additive ? [value, entry.value].compactMap { $0 }.joined(separator: " ") : entry.value
1003 }
1004 }
1005 return value.map(Params.parse)
1006 }
1007 }
1008
1009 /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and
1010 /// parentheses.
1011 /// A Lisp string's contents as `read` gives them.
1012 static func unescape(_ s: String) -> String {
1013 var out = ""
1014 var escaped = false
1015 for c in s {
1016 if escaped {
1017 switch c {
1018 case "n": out.append("\n")
1019 case "t": out.append("\t")
1020 default: out.append(c)
1021 }
1022 escaped = false
1023 } else if c == "\\" {
1024 escaped = true
1025 } else {
1026 out.append(c)
1027 }
1028 }
1029 return out
1030 }
1031
1032 /// `org-babel-parse-multiple-vars`: one `:var` value holding several assignments split
1033 /// on spaces outside quotes and brackets, pieces split beside `=` joined again.
1034 static func splitVars(_ value: String) -> [String] {
1035 var pieces: [String] = []
1036 var current = ""
1037 var depth = 0
1038 var quoted = false
1039 var previous: Character = " "
1040 for c in value {
1041 if c == "\"", previous != "\\" { quoted.toggle() }
1042 previous = c
1043 if !quoted, "([{".contains(c) { depth += 1 }
1044 if !quoted, ")]}".contains(c) { depth = max(0, depth - 1) }
1045 if c == " ", !quoted, depth == 0 {
1046 if !current.isEmpty { pieces.append(current) }
1047 current = ""
1048 } else {
1049 current.append(c)
1050 }
1051 }
1052 if !current.isEmpty { pieces.append(current) }
1053 var joined: [String] = []
1054 for piece in pieces {
1055 if let last = joined.last, last.hasSuffix("=") || piece.hasPrefix("=") {
1056 joined[joined.count - 1] = last + piece
1057 } else {
1058 joined.append(piece)
1059 }
1060 }
1061 return joined.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
1062 }
1063
1064 static func parse(_ text: String) -> [(String, String)] {
1065 var parts: [String] = []
1066 var current = ""
1067 var depth = 0
1068 var quoted = false
1069 var previous: Character = " "
1070 for c in text {
1071 if c == "\"", previous != "\\" { quoted.toggle() }
1072 if !quoted {
1073 if c == "(" { depth += 1 }
1074 if c == ")" { depth = max(0, depth - 1) }
1075 }
1076 if c == ":", !quoted, depth == 0, previous == " " || previous == "\t" {
1077 parts.append(current)
1078 current = ""
1079 }
1080 current.append(c)
1081 previous = c
1082 }
1083 parts.append(current)
1084 var pairs: [(String, String)] = []
1085 for part in parts {
1086 let trimmed = part.trimmingCharacters(in: .whitespaces)
1087 guard trimmed.hasPrefix(":") else { continue }
1088 let key = String(trimmed.prefix { !$0.isWhitespace }).lowercased()
1089 // Quotes stay until the value is used, so a quoted string isn't taken for Lisp.
1090 pairs.append((key, trimmed.dropFirst(key.count).trimmingCharacters(in: .whitespaces)))
1091 }
1092 return pairs
1093 }
1094 }
1095
1096 // MARK: - Variables
1097
1098 /// A referenced block's result as a `:var` value.
1099 static func varValue(_ finish: BabelJob.Finish, _ output: BabelOutput) -> VarValue {
1100 switch value(finish, output) {
1101 case .none: return .scalar("")
1102 case .string(let s): return number(s).map(VarValue.number) ?? .scalar(s)
1103 case .table(let rows): return .table(rows.map { $0?.map(cell) })
1104 }
1105 }
1106
1107 enum VarValue: Equatable, Sendable {
1108 case scalar(String)
1109 case number(String)
1110 /// Rows; nil is a rule (`hline`).
1111 case table([[Cell]?])
1112 case list([Cell])
1113 }
1114
1115 enum Cell: Equatable, Sendable {
1116 case text(String)
1117 case number(String)
1118 }
1119
1120 /// `org-babel--string-to-number`, keeping Emacs's printed form.
1121 static func number(_ s: String) -> String? {
1122 let t = s.trimmingCharacters(in: .whitespaces)
1123 guard !t.isEmpty, !t.contains(where: \.isWhitespace), t.range(of: "^[0-9e.+ -]+$", options: .regularExpression) != nil else { return nil }
1124 if t.range(of: "^[-+]?[0-9]+\\.?$", options: .regularExpression) != nil {
1125 var digits = t.hasSuffix(".") ? String(t.dropLast()) : t
1126 let negative = digits.hasPrefix("-")
1127 if digits.hasPrefix("-") || digits.hasPrefix("+") { digits.removeFirst() }
1128 digits = String(digits.drop { $0 == "0" })
1129 if digits.isEmpty { return "0" }
1130 return (negative ? "-" : "") + digits
1131 }
1132 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,
1133 let d = Double(t) else { return nil }
1134 return emacsFloat(d)
1135 }
1136
1137 /// How Emacs prints a float.
1138 static func emacsFloat(_ d: Double) -> String {
1139 if d == d.rounded(), abs(d) < 1e16 { return String(format: "%.1f", d) }
1140 return "\(d)"
1141 }
1142
1143 static func cell(_ s: String) -> Cell {
1144 var t = s
1145 if let m = s.firstMatch(of: /^\s*"(.+)"\s*$/) { t = String(m.1) }
1146 return number(t).map(Cell.number) ?? .text(t)
1147 }
1148
1149 /// `name` or `name(args)` naming a src block in the file: the block and its call arguments.
1150 static func referencedBlock(_ value: String, model: DocumentModel) -> (block: SrcBlockInfo, arguments: String?)? {
1151 guard let m = value.firstMatch(of: /^([^\s()\[\]"]+)(?:\((.*)\))?$/),
1152 let block = model.srcBlocks.first(where: { $0.name == String(m.1) }) else { return nil }
1153 return (block, m.2.map(String.init))
1154 }
1155
1156 /// The blocks `params`' references run, directly or through theirs.
1157 static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] {
1158 var found: [SrcBlockInfo] = []
1159 var seen = seen
1160 for value in params.vars.map(\.value) + [params.single[":stdin"]].compactMap({ $0 }) {
1161 guard let (block, arguments) = referencedBlock(value, model: model), let name = block.name, seen.insert(name).inserted else { continue }
1162 found.append(block)
1163 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? []
1164 if let inner = try? Params(block: block, model: model, text: text, extra: extra) {
1165 found += referencedBlocks(inner, model: model, text: text, seen: seen)
1166 }
1167 }
1168 return found
1169 }
1170
1171 /// `org-babel-ref-resolve` for literals, named tables and lists in the file, and named src
1172 /// blocks, which run first (`Needed`) and give their result.
1173 static func resolve(_ value: String, model: DocumentModel, text: NSString, refs: References) throws -> VarValue {
1174 if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(Params.unescape(String(value.dropFirst().dropLast()))) }
1175 if let n = number(value) { return .number(n) }
1176 if let first = value.first, "('`[".contains(first) { return try lispValue(value) }
1177 if let known = refs.resolved[value] { return known }
1178 if let (block, arguments) = referencedBlock(value, model: model), let name = block.name {
1179 guard !refs.visiting.contains(name) else { throw Failure.message("\(name) refers to itself through :var") }
1180 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? []
1181 var inner = refs
1182 inner.visiting.insert(name)
1183 var params = try Params(block: block, model: model, text: text, extra: extra)
1184 params.evaluateLisp(file: refs.options.file)
1185 // A cached block gives its result without running, as `org-babel-execute-src-block` does.
1186 var useCache = true
1187 if params.single[":cache"] == "yes", let language = block.language {
1188 let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params)
1189 let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block)
1190 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text, options: refs.options)
1191 if state.current {
1192 if let value = state.value { return value }
1193 useCache = false
1194 }
1195 }
1196 switch plan(block, params: params, model: model, text: text,
1197 options: refs.options, location: block.range, name: name, refs: inner, useCache: useCache) {
1198 case .failure(let failure): throw failure
1199 case .success(let job):
1200 throw Needed(need: job.needs.first ?? BabelJob.Need(reference: value, job: job))
1201 }
1202 }
1203 if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") }
1204 // A named element: `#+NAME: value` above a table or list.
1205 let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$"
1206 guard let r = (text as String).range(of: pattern, options: .regularExpression) else {
1207 throw Failure.message("Can't find \(value) for :var.")
1208 }
1209 let start = NSMaxRange(text.lineRange(for: NSRange(r, in: text as String)))
1210 var location = start
1211 var rows: [[Cell]?] = []
1212 var items: [Cell] = []
1213 while location < text.length {
1214 let lineRange = text.lineRange(for: NSRange(location: location, length: 0))
1215 let line = text.substring(with: lineRange).trimmingCharacters(in: .newlines)
1216 let trimmed = line.trimmingCharacters(in: .whitespaces)
1217 if trimmed.hasPrefix("|") {
1218 if trimmed.hasPrefix("|-") {
1219 rows.append(nil)
1220 location = NSMaxRange(lineRange)
1221 continue
1222 }
1223 let cells = trimmed.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
1224 rows.append((trimmed.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell))
1225 } else if let m = trimmed.firstMatch(of: /^[-+*] (.*)$/), rows.isEmpty {
1226 items.append(cell(String(m.1)))
1227 } else if trimmed.hasPrefix("#+") || (!rows.isEmpty || !items.isEmpty) {
1228 if rows.isEmpty, items.isEmpty, trimmed.lowercased().hasPrefix("#+begin_src") {
1229 throw Failure.message("Using a src block's result in :var isn't supported yet.")
1230 }
1231 break
1232 } else {
1233 break
1234 }
1235 location = NSMaxRange(lineRange)
1236 }
1237 if !rows.isEmpty { return .table(rows) }
1238 if !items.isEmpty { return .list(items) }
1239 throw Failure.message("\(value) isn't a table or list.")
1240 }
1241
1242 /// `org-babel-sh-var-to-string`: a table's rows as tab-separated lines, a list's items as
1243 /// lines.
1244 static func shellString(_ value: VarValue, separator: String? = nil, hline: String? = nil) -> String {
1245 switch value {
1246 case .scalar(let s), .number(let s): return s
1247 case .list(let items): return items.map(cellText).joined(separator: "\n")
1248 case .table(let rows): return rows.compactMap { $0?.map(cellText).joined(separator: separator ?? "\t") ?? hline }.joined(separator: "\n")
1249 }
1250 }
1251
1252 /// `org-babel-variable-assignments:shell`, by the shell's name: for bash, an array for a
1253 /// list and an associative array for a table of two or more columns; `set` for fish; a
1254 /// quoted string otherwise.
1255 static func shellAssignments(_ vars: [(String, VarValue)], shell: String, params: Params) -> [String] {
1256 let separator = params.single[":separator"]
1257 let hline = params.single[":hlines"] == "yes" ? params.single[":hline-string"] ?? "hline" : nil
1258 func quoted(_ value: VarValue) -> String {
1259 "'" + shellString(value, separator: separator, hline: hline).replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
1260 }
1261 return vars.map { name, value in
1262 if shell.hasSuffix("bash") {
1263 switch value {
1264 case .table(let rows) where (rows.first ?? nil).map({ $0.count >= 2 }) ?? false:
1265 let entries = rows.compactMap { $0 }.map { row in
1266 "\(name)[\(quoted(.scalar(cellText(row[0]))))]=\(quoted(.list(Array(row.dropFirst()))))"
1267 }
1268 return (["unset \(name)", "declare -A \(name)"] + entries).joined(separator: "\n")
1269 case .table(let rows):
1270 let items = rows.map { $0.map { quoted(.list($0)) } ?? quoted(.scalar("hline")) }
1271 return "unset \(name)\ndeclare -a \(name)=( \(items.joined(separator: " ")) )"
1272 case .list(let items):
1273 return "unset \(name)\ndeclare -a \(name)=( \(items.map { quoted(.scalar(cellText($0))) }.joined(separator: " ")) )"
1274 case .scalar, .number:
1275 break
1276 }
1277 }
1278 return (shell.hasSuffix("fish") ? "set \(name) " : "\(name)=") + quoted(value)
1279 }
1280 }
1281
1282 /// `org-babel-python-var-to-python`.
1283 static func pythonLiteral(_ value: VarValue) -> String {
1284 func cell(_ c: Cell) -> String {
1285 switch c {
1286 case .number(let n): return n
1287 case .text(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
1288 }
1289 }
1290 switch value {
1291 case .scalar(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
1292 case .number(let n): return n
1293 case .list(let items): return "[" + items.map(cell).joined(separator: ", ") + "]"
1294 case .table(let rows): return "[" + rows.map { $0.map { "[" + $0.map(cell).joined(separator: ", ") + "]" } ?? "None" }.joined(separator: ", ") + "]"
1295 }
1296 }
1297
1298 /// `prin1` of a string.
1299 static func lispString(_ s: String, triple: Bool = false) -> String {
1300 let quoted = "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
1301 return triple ? "\"\"" + quoted + "\"\"" : quoted
1302 }
1303
1304 static func lispLiteral(_ value: VarValue) -> String {
1305 func cell(_ c: Cell) -> String {
1306 switch c {
1307 case .number(let n): return n
1308 case .text(let s): return lispString(s)
1309 }
1310 }
1311 switch value {
1312 case .scalar(let s): return lispString(s)
1313 case .number(let n): return n
1314 case .list(let items): return "(" + items.map(cell).joined(separator: " ") + ")"
1315 case .table(let rows): return "(" + rows.map { $0.map { "(" + $0.map(cell).joined(separator: " ") + ")" } ?? "hline" }.joined(separator: " ") + ")"
1316 }
1317 }
1318
1319 // MARK: - Python
1320
1321 /// Writes the value of `main()` to the result file as org's Python wrapper does: `pprint`
1322 /// for `pp`, `key :: value` items for a dict as a list, dicts as rows for tables,
1323 /// otherwise `str`.
1324 static let pythonFormatter = """
1325 def __orgstar_format(value, path, params):
1326 def rows(v):
1327 if isinstance(v, dict):
1328 return [(k, rows(x)) for k, x in v.items()]
1329 if isinstance(v, (list, tuple)):
1330 return [rows(x) for x in v]
1331 return v
1332 with open(path, 'w') as out:
1333 if 'pp' in params:
1334 import pprint
1335 out.write(pprint.pformat(value))
1336 elif 'list' in params and isinstance(value, dict):
1337 out.write(str(['{} :: {}'.format(k, v) for k, v in value.items()]))
1338 else:
1339 if not set(params) & {'scalar', 'verbatim', 'raw'} and 'table' in params:
1340 value = rows(value)
1341 out.write(str(value))
1342
1343 """
1344
1345 /// `org-babel-python--shift-right`: four spaces more on every line outside strings.
1346 static func shiftRight(_ body: String) -> String {
1347 var inString: String?
1348 var lines: [String] = []
1349 for line in body.components(separatedBy: "\n") {
1350 lines.append(inString == nil ? " " + line : line)
1351 var i = line.startIndex
1352 while i < line.endIndex {
1353 if let delimiter = inString {
1354 if line[i...].hasPrefix(delimiter) {
1355 inString = nil
1356 i = line.index(i, offsetBy: 3)
1357 continue
1358 }
1359 } else if line[i] == "#" {
1360 break
1361 } else if line[i...].hasPrefix("\"\"\"") || line[i...].hasPrefix("'''") {
1362 inString = String(line[i...].prefix(3))
1363 i = line.index(i, offsetBy: 3)
1364 continue
1365 } else if line[i] == "\"" || line[i] == "'" {
1366 let quote = line[i]
1367 i = line.index(after: i)
1368 while i < line.endIndex, line[i] != quote {
1369 if line[i] == "\\" { i = line.index(after: i) }
1370 if i < line.endIndex { i = line.index(after: i) }
1371 }
1372 }
1373 if i < line.endIndex { i = line.index(after: i) }
1374 }
1375 }
1376 return lines.joined(separator: "\n")
1377 }
1378
1379 // MARK: - Emacs Lisp
1380
1381 /// Evaluates `form` and prints the result as JSON: its `%s` and `%S` forms, and its rows
1382 /// when it is a table.
1383 static func emacsScript(form: String, params: [String]) -> String {
1384 """
1385 ;;; -*- lexical-binding: t -*-
1386 (require 'cl-lib)
1387 (require 'json)
1388 (let* ((result \(form))
1389 (print-level nil)
1390 (print-length nil)
1391 (cell (lambda (c) (if (stringp c) c (format "%s" c))))
1392 (tabulable (and (proper-list-p result)
1393 (cl-every (lambda (e) (or (atom e) (proper-list-p e))) result)))
1394 (rows (and tabulable result
1395 (if (cl-every (lambda (e) (or (eq e 'hline) (listp e))) result)
1396 (mapcar (lambda (r) (if (eq r 'hline) "hline" (vconcat (mapcar cell r)))) result)
1397 (list (vconcat (mapcar cell result)))))))
1398 (princ (json-encode `((s . ,(format "%s" result))
1399 (S . ,(format "%S" result))
1400 (string . ,(if (stringp result) t :json-false))
1401 (rows . ,(if rows (vconcat rows) :json-null))))))
1402 """
1403 }
1404
1405 /// An Emacs Lisp job run by `Elisp` instead of Emacs: the output `emacsScript` would print.
1406 /// A Lisp error fails the run, as it does in Emacs; nil when the block uses what `Elisp`
1407 /// doesn't have.
1408 public static func evaluateNatively(_ job: BabelJob) throws(Failure) -> BabelOutput? {
1409 guard let form = job.lispForm, let sexp = try? LispReader.readFirst(form).sexp else { return nil }
1410 let lisp = Elisp()
1411 let result: Sexp
1412 do {
1413 result = try lisp.eval(sexp)
1414 } catch is Elisp.Unsupported {
1415 return nil
1416 } catch {
1417 throw .message("\(error)")
1418 }
1419 func cell(_ c: Sexp) -> String { Elisp.printed(c) }
1420 func isProper(_ v: Sexp) -> Bool {
1421 if case .list = v { return true }
1422 return Elisp.isNil(v)
1423 }
1424 func isAtom(_ v: Sexp) -> Bool {
1425 switch v {
1426 case .list(let items): items.isEmpty
1427 case .dotted: false
1428 default: true
1429 }
1430 }
1431 var rows: [Any] = []
1432 if case .list(let items) = result, items.allSatisfy({ isAtom($0) || isProper($0) }) {
1433 if items.allSatisfy({ $0 == .symbol("hline") || isProper($0) }) {
1434 rows = items.map { $0 == .symbol("hline") ? "hline" as Any : ((try? Elisp.elements($0)) ?? []).map(cell) as Any }
1435 } else {
1436 rows = [items.map(cell)]
1437 }
1438 }
1439 var object: [String: Any] = ["s": Elisp.printed(result), "S": Elisp.printed(result, escape: true), "string": result.string != nil]
1440 object["rows"] = rows.isEmpty ? NSNull() : rows
1441 guard let data = try? JSONSerialization.data(withJSONObject: object), let json = String(data: data, encoding: .utf8) else { return nil }
1442 return BabelOutput(stdout: lisp.standardOutput + json, stderr: lisp.messages)
1443 }
1444
1445 // MARK: - Finishing
1446
1447 /// What the run produced, as Babel holds it before inserting.
1448 enum Value: Equatable {
1449 case none
1450 case string(String)
1451 /// Rows of cells; nil rows are rules.
1452 case table([[String]?])
1453 }
1454
1455 /// The edit that writes the run's result under the block, or why it can't.
1456 public static func finish(_ job: BabelJob, output: BabelOutput, text: String, settings: OrgSettings = .default) -> Result<[TextEdit], Failure> {
1457 let ns = text as NSString
1458 var blockRange = job.block
1459 if blockRange.upperBound > ns.length || ns.substring(with: NSRange(blockRange)) != job.blockText {
1460 let found = ns.range(of: job.blockText)
1461 guard found.location != NSNotFound,
1462 ns.range(of: job.blockText, range: NSRange(location: NSMaxRange(found), length: ns.length - NSMaxRange(found))).location == NSNotFound else {
1463 return .failure(.message("The block changed while it ran; the result wasn't written."))
1464 }
1465 blockRange = found.location..<NSMaxRange(found)
1466 }
1467 let value = Self.value(job.finish, output)
1468 if job.finish.params.contains("silent") || job.finish.params.contains("none") || job.finish.params.contains("discard") { return .success([]) }
1469 let buffer = EmacsBuffer(text, point: 0, settings: settings)
1470 if job.finish.inline {
1471 do {
1472 try insertInlineResult(value, job.finish, at: blockRange, buffer: buffer)
1473 } catch let error as Failure {
1474 return .failure(error)
1475 } catch {
1476 return .failure(.message("\(error)"))
1477 }
1478 return .success(TextEdit.between(text, buffer.text as String))
1479 }
1480 insertResult(value, job.finish, block: blockRange, buffer: buffer)
1481 return .success(TextEdit.between(text, buffer.text as String))
1482 }
1483
1484 /// The language's result handling: `org-babel-result-cond` and the table import.
1485 static func value(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value {
1486 reassemble(rawValue(finish, output), finish)
1487 }
1488
1489 static func rawValue(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value {
1490 let params = finish.params
1491 if params.contains("file") {
1492 // `org-babel-result-to-file`: a link to `:file`, or to the result as a path.
1493 let path = finish.file ?? rawResult(finish, output)
1494 var link = "[[file:" + path + "]"
1495 if let description = finish.fileDescription { link += "[" + (description.isEmpty ? path : description) + "]" }
1496 return .string(link + "]")
1497 }
1498 let scalar = params.contains("scalar") || params.contains("verbatim") || params.contains("html") || params.contains("code")
1499 || params.contains("pp") || params.contains("file")
1500 || ((params.contains("output") || params.contains("raw") || params.contains("org")) && !params.contains("table"))
1501 switch finish.kind {
1502 case .shell(let valueIsExitStatus):
1503 var results = output.stdout
1504 if valueIsExitStatus {
1505 results = results.split(separator: "\n", omittingEmptySubsequences: true).last.map(String.init) ?? ""
1506 }
1507 if scalar { return .string(results) }
1508 return importTable(results)
1509 case .generic:
1510 return .string(output.stdout)
1511 case .python:
1512 let raw = finish.output ? output.stdout : (output.resultFile ?? "")
1513 if scalar { return .string(raw) }
1514 return pythonValue(raw)
1515 case .emacsLisp:
1516 guard let data = output.stdout.data(using: .utf8),
1517 let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return .string(output.stdout) }
1518 if scalar {
1519 return .string((params.contains("scalar") || params.contains("verbatim") ? object["S"] : object["s"]) as? String ?? "")
1520 }
1521 if object["string"] as? Bool == true { return .string(object["s"] as? String ?? "") }
1522 if let rows = object["rows"] as? [Any] {
1523 return .table(rows.map { $0 as? String == "hline" ? nil : ($0 as? [String] ?? []) })
1524 }
1525 let printed = object["S"] as? String ?? ""
1526 return printed == "nil" ? .none : .string(printed)
1527 }
1528 }
1529
1530 /// The result as the program left it, before tables are read.
1531 static func rawResult(_ finish: BabelJob.Finish, _ output: BabelOutput) -> String {
1532 switch finish.kind {
1533 case .python: return finish.output ? output.stdout : (output.resultFile ?? "")
1534 case .emacsLisp:
1535 guard let data = output.stdout.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return output.stdout }
1536 return object["s"] as? String ?? ""
1537 case .shell(let valueIsExitStatus):
1538 return valueIsExitStatus ? output.stdout.split(separator: "\n").last.map(String.init) ?? "" : output.stdout
1539 case .generic: return output.stdout
1540 }
1541 }
1542
1543 /// With `:results file` and `:file`, what to write there, relative to the run's folder;
1544 /// nil when there's nothing to write or the program wrote it.
1545 public static func fileResult(_ job: BabelJob, output: BabelOutput) -> (path: String, contents: String)? {
1546 guard job.finish.params.contains("file"), let file = job.finish.file, !job.finish.writesFile else { return nil }
1547 return (file, rawResult(job.finish, output))
1548 }
1549
1550 /// `org-babel-import-elisp-from-file`.
1551 static func importTable(_ results: String) -> Value {
1552 guard !results.isEmpty else { return .none }
1553 var lines = results.components(separatedBy: "\n")
1554 if lines.last == "" { lines.removeLast() }
1555 let nonEmpty = lines.filter { !$0.isEmpty }
1556 let rows: [[String]]
1557 if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains("\t") }) {
1558 rows = lines.map { $0.components(separatedBy: "\t") }
1559 } else if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains(",") }) {
1560 rows = lines.map(csvFields)
1561 } else if lines.count == 1 {
1562 rows = [[lines[0]]]
1563 } else {
1564 rows = lines.map { line in
1565 let trimmed = line.drop { $0 == " " }
1566 return trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init).isEmpty
1567 ? [""] : trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
1568 }
1569 }
1570 let cells = rows.map { row in row.map { field -> String in
1571 let trimmed = field.trimmingCharacters(in: .whitespaces)
1572 switch cell(trimmed) {
1573 case .number(let n): return n
1574 case .text(let t): return t
1575 }
1576 } }
1577 if cells.count == 1, cells[0].count == 1 { return .string(cells[0][0]) }
1578 return .table(cells)
1579 }
1580
1581 static func csvFields(_ line: String) -> [String] {
1582 var fields: [String] = []
1583 var current = ""
1584 var quoted = false
1585 for c in line {
1586 if c == "\"" {
1587 quoted.toggle()
1588 } else if c == ",", !quoted {
1589 fields.append(current.trimmingCharacters(in: .whitespaces))
1590 current = ""
1591 } else {
1592 current.append(c)
1593 }
1594 }
1595 fields.append(current.trimmingCharacters(in: .whitespaces))
1596 return fields
1597 }
1598
1599 /// `org-babel-python-table-or-string`.
1600 static func pythonValue(_ raw: String) -> Value {
1601 guard let first = raw.first, let last = raw.last, raw.count >= 2,
1602 (first == "[" && last == "]") || (first == "(" && last == ")") else {
1603 if raw.isEmpty { return .string("") }
1604 if raw.count > 2, (raw.hasPrefix("'") && raw.hasSuffix("'")) || (raw.hasPrefix("\"") && raw.hasSuffix("\"")) {
1605 return .string(String(raw.dropFirst().dropLast()))
1606 }
1607 if let n = number(raw) { return .string(n) }
1608 return .string(raw)
1609 }
1610 var parser = PythonLiteral(Array(raw))
1611 guard let parsed = parser.parse(), parser.atEnd, case .list(let items) = parsed else { return .string(raw) }
1612 func text(_ v: PythonLiteral.Node) -> String {
1613 switch v {
1614 case .atom(let s): return s
1615 case .string(let s): return s
1616 case .list(let xs): return "(" + xs.map(text).joined(separator: " ") + ")"
1617 }
1618 }
1619 if items.allSatisfy({ if case .list = $0 { return true }; if case .atom("None") = $0 { return true }; return false }) {
1620 return .table(items.map { item -> [String]? in
1621 if case .list(let cells) = item { return cells.map(text) }
1622 return nil
1623 })
1624 }
1625 return .table([items.map { if case .atom("None") = $0 { return "hline" }; return text($0) }])
1626 }
1627
1628 struct PythonLiteral {
1629 indirect enum Node: Equatable {
1630 case atom(String)
1631 case string(String)
1632 case list([Node])
1633 }
1634
1635 let chars: [Character]
1636 var i = 0
1637 init(_ chars: [Character]) { self.chars = chars }
1638 var atEnd: Bool { mutating get { skip(); return i == chars.count } }
1639
1640 mutating func skip() { while i < chars.count, chars[i] == " " || chars[i] == "\n" { i += 1 } }
1641
1642 mutating func parse() -> Node? {
1643 skip()
1644 guard i < chars.count else { return nil }
1645 let c = chars[i]
1646 if c == "[" || c == "(" {
1647 let close: Character = c == "[" ? "]" : ")"
1648 i += 1
1649 var items: [Node] = []
1650 skip()
1651 if i < chars.count, chars[i] == close {
1652 i += 1
1653 return .list(items)
1654 }
1655 while true {
1656 guard let item = parse() else { return nil }
1657 items.append(item)
1658 skip()
1659 guard i < chars.count else { return nil }
1660 if chars[i] == "," {
1661 i += 1
1662 skip()
1663 if i < chars.count, chars[i] == close {
1664 i += 1
1665 return .list(items)
1666 }
1667 continue
1668 }
1669 if chars[i] == close {
1670 i += 1
1671 return .list(items)
1672 }
1673 return nil
1674 }
1675 }
1676 if c == "'" || c == "\"" {
1677 i += 1
1678 var s = ""
1679 while i < chars.count, chars[i] != c {
1680 if chars[i] == "\\", i + 1 < chars.count {
1681 i += 1
1682 switch chars[i] {
1683 case "n": s.append("\n")
1684 case "t": s.append("\t")
1685 default: s.append(chars[i])
1686 }
1687 } else {
1688 s.append(chars[i])
1689 }
1690 i += 1
1691 }
1692 guard i < chars.count else { return nil }
1693 i += 1
1694 return .string(s)
1695 }
1696 var s = ""
1697 while i < chars.count, !",)] \n".contains(chars[i]) {
1698 s.append(chars[i])
1699 i += 1
1700 }
1701 guard !s.isEmpty else { return nil }
1702 return .atom(number(s) ?? s)
1703 }
1704 }
1705
1706 // MARK: - Inserting
1707
1708 static let resultKeyword = "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:"
1709
1710 /// `org-babel-where-is-src-block-result` with `insert`: the line of the block's results
1711 /// keyword, added when there is none.
1712 /// The `#+RESULTS` line of a block's results (`org-babel-where-is-src-block-result`):
1713 /// the one naming `name`, or for an unnamed block an empty one right after it.
1714 static func existingResults(name: String?, block: Range<Int>, text ns: NSString) -> Int? {
1715 if let name {
1716 let pattern = "(?im)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*" + NSRegularExpression.escapedPattern(for: name) + "[ \\t]*$"
1717 let regex = try! NSRegularExpression(pattern: pattern)
1718 return regex.firstMatch(in: ns as String, range: NSRange(location: 0, length: ns.length))?.range.location
1719 }
1720 // The element after the block, past blank lines.
1721 var location = block.upperBound
1722 while location < ns.length {
1723 let line = ns.lineRange(for: NSRange(location: location, length: 0))
1724 let content = ns.substring(with: line)
1725 if content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
1726 location = NSMaxRange(line)
1727 continue
1728 }
1729 if content.range(of: "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*\\n?$", options: .regularExpression) != nil {
1730 return line.location
1731 }
1732 break
1733 }
1734 return nil
1735 }
1736
1737 /// The hash in the `#+RESULTS[…]:` line at `line` (`org-babel-result-regexp`).
1738 static func resultsHash(at line: Int, text ns: NSString) -> String? {
1739 let content = ns.substring(with: ns.lineRange(for: NSRange(location: line, length: 0)))
1740 return content.firstMatch(of: #/(?i)^[ \t]*#\+results\[(?:\(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\) )?([0-9a-f]+)\]:/#).map { String($0.1) }
1741 }
1742
1743 static func resultsLine(_ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) -> Int {
1744 if let line = existingResults(name: finish.name, block: block, text: buffer.text) {
1745 // `org-babel--clear-results-maybe`: a different hash clears the results and
1746 // rewrites the keyword.
1747 if let hash = finish.hash, resultsHash(at: line, text: buffer.text) != hash {
1748 buffer.point = line
1749 let indent = String(buffer.substring(line, buffer.lineEnd(line)).prefix { $0 == " " || $0 == "\t" })
1750 buffer.forwardLine(1)
1751 let contents = buffer.point
1752 buffer.deleteRegion(contents, max(contents, resultEnd(buffer)))
1753 buffer.deleteRegion(line, contents)
1754 buffer.point = line
1755 buffer.insert(indent + "#+RESULTS[\(hash)]:" + (finish.name.map { " " + $0 } ?? "") + "\n")
1756 if buffer.looking("^[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil {
1757 buffer.saveExcursion { buffer.insert("\n") }
1758 }
1759 }
1760 return line
1761 }
1762 // A new keyword after the block.
1763 buffer.point = min(block.upperBound, buffer.length)
1764 buffer.skipBackward([32, 9, 10, 13])
1765 buffer.forwardLine(1)
1766 if !buffer.bolp { buffer.insert("\n") }
1767 let indent = buffer.saveExcursion { () -> String in
1768 buffer.point = block.lowerBound
1769 let line = buffer.substring(buffer.lineStart(buffer.point), buffer.lineEnd(buffer.point))
1770 return String(line.prefix { $0 == " " || $0 == "\t" })
1771 }
1772 buffer.insert("\n")
1773 let keywordLine = buffer.point
1774 buffer.insert(indent + "#+RESULTS" + (finish.hash.map { "[\($0)]" } ?? "") + ":" + (finish.name.map { " " + $0 } ?? "") + "\n")
1775 if buffer.looking("^[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil {
1776 buffer.saveExcursion { buffer.insert("\n") }
1777 }
1778 return keywordLine
1779 }
1780
1781 /// `org-babel-result-end` with point at the start of the result.
1782 static func resultEnd(_ buffer: EmacsBuffer) -> Int {
1783 let start = buffer.point
1784 let line = buffer.substring(buffer.lineStart(start), buffer.lineEnd(start))
1785 if line.trimmingCharacters(in: .whitespaces).isEmpty { return start }
1786 if line.range(of: "^[ \\t]*\\[\\[[^\\]]+\\](\\[[^\\]]*\\])?\\][ \\t]*$", options: .regularExpression) != nil {
1787 return min(buffer.length, buffer.lineEnd(start) + 1)
1788 }
1789 let tree = OrgParser.parse(buffer.text as String)
1790 var node = tree.root
1791 var found: SyntaxNode?
1792 while let child = node.child(containing: start) {
1793 if child.range.lowerBound == start || (buffer.lineStart(child.range.lowerBound) == start) {
1794 if [.fixedWidth, .table, .plainList, .drawer, .block, .dynamicBlock, .latexFragment].contains(child.kind) {
1795 found = child
1796 break
1797 }
1798 }
1799 node = child
1800 }
1801 guard let element = found else { return start }
1802 var end = element.range.upperBound
1803 while end > start, let c = buffer.character(at: end - 1), c == 32 || c == 9 || c == 10 || c == 13 { end -= 1 }
1804 return min(buffer.length, buffer.lineEnd(end) + 1)
1805 }
1806
1807 /// `org-babel-insert-result` for a src block.
1808 /// An inline block's result, after it: ` {{{results(=value=)}}}`, replacing one already
1809 /// there; raw results go in as they are.
1810 static func insertInlineResult(_ value: Value, _ finish: BabelJob.Finish, at object: Range<Int>, buffer: EmacsBuffer) throws {
1811 var string: String
1812 switch value {
1813 case .none: string = ""
1814 case .string(let s): string = s
1815 case .table(let rows):
1816 let cells = rows.compactMap { $0 }
1817 guard cells.count == 1, cells[0].count == 1 else { throw Failure.message("Inline source block results can't be tables or lists") }
1818 string = cells[0][0]
1819 }
1820 buffer.point = object.upperBound
1821 if let existing = buffer.lookingAt("[ \\t]*\\{\\{\\{results\\(.*?\\)\\}\\}\\}") {
1822 buffer.deleteRegion(existing[0]!.lowerBound, existing[0]!.upperBound)
1823 }
1824 if finish.params.contains("raw") {
1825 buffer.insert(" " + string)
1826 return
1827 }
1828 if string.hasSuffix("\n") { string.removeLast() }
1829 guard !string.contains("\n") else { throw Failure.message("Inline source block results must be one line") }
1830 buffer.insert(" {{{results(=" + string + "=)}}}")
1831 }
1832
1833 static func insertResult(_ value: Value, _ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) {
1834 let params = finish.params
1835 let keyword = resultsLine(finish, block: block, buffer: buffer)
1836 buffer.point = keyword
1837 let indent = buffer.substring(buffer.lineStart(keyword), buffer.lineEnd(keyword)).prefix { $0 == " " || $0 == "\t" }.count
1838 buffer.forwardLine(1)
1839 if buffer.point == buffer.lineEnd(keyword) { buffer.insert("\n") }
1840 var beg = buffer.point
1841 if params.contains("replace") {
1842 buffer.deleteRegion(beg, resultEnd(buffer))
1843 buffer.point = beg
1844 } else if params.contains("append") {
1845 buffer.point = resultEnd(buffer)
1846 beg = buffer.point
1847 }
1848 var tabulated = false
1849 switch value {
1850 case .none:
1851 break
1852 case .string(var s) where params.contains("list"):
1853 if s.last != "\n" { s += "\n" }
1854 let items = s.split(separator: "\n", omittingEmptySubsequences: true)
1855 buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
1856 case .table(let rows) where params.contains("list"):
1857 let items = rows.count == 1 ? (rows[0] ?? []) : rows.compactMap { $0?.joined(separator: " ") }
1858 buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
1859 case .table(let rows):
1860 tabulated = true
1861 let lines = rows.map { row in row.map { "| " + $0.map { " " + $0 + " " }.joined(separator: "|").dropFirst() + " |" } ?? "|-" }
1862 let start = buffer.point
1863 buffer.insert(lines.joined(separator: "\n") + "\n")
1864 buffer.point = start
1865 buffer.tableAlign()
1866 buffer.point = buffer.tableEnd()
1867 case .string(var s):
1868 if !s.isEmpty, s.last != "\n" { s += "\n" }
1869 buffer.insert(s)
1870 }
1871 let end = buffer.marker(buffer.point, advances: true)
1872 defer { buffer.release(end) }
1873 func wrap(_ open: String, _ close: String, escape: Bool) {
1874 buffer.point = end.position
1875 buffer.insert(close + "\n")
1876 buffer.point = beg
1877 buffer.insert(open + "\n")
1878 if escape { escapeCode(buffer, from: buffer.point, to: end.position - close.utf16.count - 1) }
1879 }
1880 if let wrapValue = finish.wrap {
1881 let full = wrapValue.isEmpty ? "results" : wrapValue
1882 let type = String(full.split(separator: " ").first ?? "results")
1883 if type.lowercased() != "nil", type.lowercased() != "no" {
1884 wrap("#+begin_" + full, "#+end_" + type, escape: !["example", "src", "export"].contains(type.lowercased()) ? false : true)
1885 }
1886 } else if params.contains("html") {
1887 wrap("#+begin_export html", "#+end_export", escape: true)
1888 } else if params.contains("latex") {
1889 wrap("#+begin_export latex", "#+end_export", escape: true)
1890 } else if params.contains("org") {
1891 wrap("#+begin_src org", "#+end_src", escape: true)
1892 } else if params.contains("code") {
1893 wrap("#+begin_src \(finish.language.isEmpty ? "none" : finish.language)", "#+end_src", escape: true)
1894 } else if params.contains("raw") || params.contains("file") {
1895 // As inserted.
1896 } else if params.contains("drawer") {
1897 wrap(":results:", ":end:", escape: false)
1898 } else if !tabulated, !params.contains("list") {
1899 examplify(buffer, beg, end.position)
1900 }
1901 if indent > 0, !(tabulated && params.contains("append")) {
1902 buffer.point = beg
1903 while buffer.point < end.position {
1904 if !buffer.eolp { buffer.insert(String(repeating: " ", count: indent)) }
1905 if buffer.forwardLine(1) != 0 { break }
1906 }
1907 }
1908 }
1909
1910 /// `org-escape-code-in-region`.
1911 static func escapeCode(_ buffer: EmacsBuffer, from start: Int, to end: Int) {
1912 buffer.point = start
1913 let stop = buffer.marker(end)
1914 defer { buffer.release(stop) }
1915 while buffer.point < stop.position {
1916 let line = buffer.substring(buffer.point, buffer.lineEnd(buffer.point))
1917 if let m = line.firstMatch(of: /^[ \t]*(,*(\*|#\+))/) {
1918 buffer.point += line[..<m.1.startIndex].utf16.count
1919 buffer.insert(",")
1920 }
1921 if buffer.forwardLine(1) != 0 { break }
1922 }
1923 }
1924
1925 /// `org-babel-examplify-region`: fewer than ten lines get `: `, more an example block.
1926 static func examplify(_ buffer: EmacsBuffer, _ beg: Int, _ end: Int) {
1927 let text = buffer.substring(beg, end)
1928 var size = text.components(separatedBy: "\n").count - (text.hasSuffix("\n") ? 1 : 0)
1929 if text.isEmpty { size = 0 }
1930 guard size > 0 else { return }
1931 if size < 10 {
1932 buffer.point = beg
1933 for _ in 0..<size {
1934 buffer.point = buffer.lineStart(buffer.point)
1935 buffer.insert(": ")
1936 buffer.forwardLine(1)
1937 }
1938 return
1939 }
1940 let endMarker = buffer.marker(end)
1941 defer { buffer.release(endMarker) }
1942 buffer.point = beg
1943 buffer.insert("#+begin_example\n")
1944 escapeCode(buffer, from: buffer.point, to: endMarker.position)
1945 buffer.point = endMarker.position
1946 if !buffer.bolp { buffer.insert("\n") }
1947 buffer.insert("#+end_example")
1948 if !buffer.eolp { buffer.insert("\n") }
1949 }
1950}
1951
1952/// C-c C-c in a src block: `org-babel-execute-src-block`.
1953public struct ExecuteSrcBlock: OrgCommand {
1954 public init() {}
1955 public var id: String { "org.babel.execute" }
1956 public var title: String { "Run Source Block" }
1957 public func applies(in context: EditContext) -> Bool { KeyContext.holds("src", in: context) || Self.atCallOrInline(context) }
1958
1959 /// On a `#+CALL:` line, an inline `call_` or an inline `src_` block.
1960 static func atCallOrInline(_ context: EditContext) -> Bool {
1961 let ns = context.text as NSString
1962 let line = ns.substring(with: ns.lineRange(for: NSRange(location: min(context.caret, ns.length), length: 0)))
1963 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil { return true }
1964 var node = context.tree.root
1965 while let child = node.child(containing: context.caret) {
1966 if child.kind == .inlineSourceBlock || child.kind == .inlineBabelCall { return true }
1967 node = child
1968 }
1969 return false
1970 }
1971
1972 public func run(in context: EditContext) -> CommandStep {
1973 switch Babel.plan(context) {
1974 case .success(let job): return .external(.babel(job))
1975 case .failure(.message(let message)): return .failed(message)
1976 }
1977 }
1978}