krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Compute/Babel.swift

1978 lines · 104234 bytes

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