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