| @@ -68,6 +68,10 @@ public struct BabelJob: Sendable, Equatable { |
| 68 | 68 | var writesFile = false |
| 69 | 69 | /// `:cache yes`: the hash for the `#+RESULTS[…]:` line. |
| 70 | 70 | var hash: String? |
| 71 | /// Column and row names a table `:var` gave up, put back on a table result |
| 72 | /// (`org-babel-reassemble-table`). |
| 73 | var colnames: [String]? |
| 74 | var rownames: [String]? |
| 71 | 75 | } |
| 72 | 76 | } |
| 73 | 77 | |
| @@ -290,14 +294,17 @@ public enum Babel { |
| 290 | 294 | } |
| 291 | 295 | let toFile = params.results.contains("file") |
| 292 | 296 | var hash: String? |
| 297 | var tables = Tables() |
| 293 | 298 | let finishBase = { (kind: BabelJob.Finish.Kind) in |
| 294 | 299 | BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline, |
| 295 | | file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash) |
| 300 | file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash, |
| 301 | colnames: tables.picked(tables.colnames, params.single[":colnames"]), rownames: tables.picked(tables.rownames, params.single[":rownames"])) |
| 296 | 302 | } |
| 297 | 303 | let vars: [(String, VarValue)] |
| 298 | 304 | var stdin: VarValue? |
| 299 | 305 | do { |
| 300 | | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) } |
| 306 | tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) }, params: params) |
| 307 | vars = tables.vars |
| 301 | 308 | stdin = try params.single[":stdin"].map { try resolve($0, model: model, text: ns, refs: refs) } |
| 302 | 309 | } catch let needed as Needed { |
| 303 | 310 | // Asked about once, with every block the references reach; asked every time when |
| @@ -318,7 +325,7 @@ public enum Babel { |
| 318 | 325 | } |
| 319 | 326 | if useCache, !inline, params.single[":cache"] == "yes" { |
| 320 | 327 | do { |
| 321 | | let state = try cacheState(language: language, params: params, vars: vars, body: body, location: location, name: name, |
| 328 | let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name, |
| 322 | 329 | call: location != block.range, text: ns) |
| 323 | 330 | if state.current { return .failure(.message(cachedMessage(state.value))) } |
| 324 | 331 | // Appended or prepended results accumulate, so they carry no hash. |
| @@ -574,13 +581,110 @@ public enum Babel { |
| 574 | 581 | return lines.joined(separator: "\n") |
| 575 | 582 | } |
| 576 | 583 | |
| 584 | // MARK: - Tables in variables |
| 585 | |
| 586 | /// Variables after `org-babel-disassemble-tables`, with the names taken off their tables. |
| 587 | struct Tables { |
| 588 | var vars: [(String, VarValue)] = [] |
| 589 | var colnames: [(name: String, names: [Cell])] = [] |
| 590 | var rownames: [(name: String, names: [Cell])] = [] |
| 591 | |
| 592 | /// `org-babel-pick-name`: the last variable's names, unless the header argument is |
| 593 | /// absent, whose `nil` selects none. |
| 594 | func picked(_ names: [(name: String, names: [Cell])], _ selector: String?) -> [String]? { |
| 595 | guard selector != nil, let last = names.last else { return nil } |
| 596 | return last.names.map(Babel.cellText) |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | static func cellText(_ c: Cell) -> String { |
| 601 | switch c { |
| 602 | case .number(let n): n |
| 603 | case .text(let s): s |
| 604 | } |
| 605 | } |
| 606 | |
| 607 | /// `org-babel-disassemble-tables`: a table's header row comes off when `:colnames` asks, or |
| 608 | /// by default when only its second row is a rule; its first column with `:rownames`; its |
| 609 | /// rules unless `:hlines yes`. |
| 610 | static func disassemble(_ vars: [(String, VarValue)], params: Params) throws -> Tables { |
| 611 | for key in [":colnames", ":rownames"] where params.lisp.contains(key) { |
| 612 | throw Failure.message("\(key) with a Lisp value isn't supported yet; nothing was run.") |
| 613 | } |
| 614 | let colnames = params.single[":colnames"] |
| 615 | let rownames = params.single[":rownames"] |
| 616 | let hlines = params.single[":hlines"] ?? "no" |
| 617 | var tables = Tables() |
| 618 | for (name, value) in vars { |
| 619 | guard case .table(var rows) = value else { |
| 620 | tables.vars.append((name, value)) |
| 621 | continue |
| 622 | } |
| 623 | let auto = rows.count > 1 && rows[0] != nil && rows[1] == nil && !rows.dropFirst(2).contains(where: { $0 == nil }) |
| 624 | if colnames != "no", colnames != nil || auto { |
| 625 | while let first = rows.first, first == nil { rows.removeFirst() } |
| 626 | if let header = rows.first ?? nil { |
| 627 | rows.removeFirst(rows.count > 1 && rows[1] == nil ? 2 : 1) |
| 628 | tables.colnames.append((name, header)) |
| 629 | } |
| 630 | } |
| 631 | if let rownames, rownames != "no" { |
| 632 | let kept = rows.compactMap { $0 } |
| 633 | tables.rownames.append((name, kept.map { $0.first ?? .text("") })) |
| 634 | rows = kept.map { Array($0.dropFirst()) } |
| 635 | } |
| 636 | if hlines != "yes" { rows = rows.filter { $0 != nil } } |
| 637 | tables.vars.append((name, .table(rows))) |
| 638 | } |
| 639 | return tables |
| 640 | } |
| 641 | |
| 642 | /// `org-babel-reassemble-table` on a result. |
| 643 | static func reassemble(_ value: Value, _ finish: BabelJob.Finish) -> Value { |
| 644 | guard case .table(var rows) = value else { return value } |
| 645 | if let names = finish.rownames, names.count == rows.count { |
| 646 | var next = names.makeIterator() |
| 647 | rows = rows.map { row in row.map { [next.next() ?? ""] + $0 } } |
| 648 | } |
| 649 | if let names = finish.colnames, let first = rows.first ?? nil, first.count == names.count { |
| 650 | rows = [names, nil] + rows |
| 651 | } |
| 652 | return .table(rows) |
| 653 | } |
| 654 | |
| 655 | /// A `:var` value that is Lisp (`org-babel-read`): evaluated, as a number, string, list or |
| 656 | /// table. |
| 657 | static func lispValue(_ value: String) throws -> VarValue { |
| 658 | let refusal = Failure.message(":var value \(value) is Lisp that only Emacs can evaluate.") |
| 659 | guard let form = try? LispReader.readFirst(value).sexp, let result = try? Elisp().eval(form) else { throw refusal } |
| 660 | func cell(_ v: Sexp) throws -> Cell { |
| 661 | if let s = v.string { return .text(s) } |
| 662 | if let n = number(Elisp.printed(v)), Elisp.printed(v) == n { return .number(n) } |
| 663 | throw refusal |
| 664 | } |
| 665 | if let s = result.string { return .scalar(s) } |
| 666 | if case .list(let items) = result, !items.isEmpty { |
| 667 | if items.allSatisfy({ if case .list = $0 { return true }; return $0 == .symbol("hline") }) { |
| 668 | return .table(try items.map { item in |
| 669 | if item == .symbol("hline") { return nil } |
| 670 | guard case .list(let cells) = item else { throw refusal } |
| 671 | return try cells.map(cell) |
| 672 | }) |
| 673 | } |
| 674 | return .list(try items.map(cell)) |
| 675 | } |
| 676 | if case .number(let n) = try? cell(result) { return .number(n) } |
| 677 | throw refusal |
| 678 | } |
| 679 | |
| 577 | 680 | // MARK: - Cache |
| 578 | 681 | |
| 579 | 682 | /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results |
| 580 | 683 | /// already carry it, with their value when it can be read. |
| 581 | | static func cacheState(language: String, params: Params, vars: [(String, VarValue)], body: String, location: Range<Int>, name: String?, |
| 684 | static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?, |
| 582 | 685 | call: Bool, text: NSString) throws -> (hash: String, current: Bool, value: VarValue?) { |
| 583 | | let input = try cacheInput(language: language, params: params, vars: vars, expanded: expandedBody(language: language, vars: vars, body: body), call: call) |
| 686 | let input = try cacheInput(language: language, params: params, tables: tables, |
| 687 | expanded: expandedBody(language: language, vars: tables.vars, body: body), call: call) |
| 584 | 688 | let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined() |
| 585 | 689 | guard let line = existingResults(name: name, block: location, text: text), resultsHash(at: line, text: text) == hash else { |
| 586 | 690 | return (hash, false, nil) |
| @@ -610,9 +714,9 @@ public enum Babel { |
| 610 | 714 | |
| 611 | 715 | /// What `org-babel-sha1-hash` hashes: the header arguments after `org-babel-process-params`, |
| 612 | 716 | /// sorted by key and printed with `%S`, then the expanded body. |
| 613 | | static func cacheInput(language: String, params: Params, vars: [(String, VarValue)], expanded: String, call: Bool) throws -> String { |
| 614 | | if !params.lisp.isEmpty || params.single[":colnames"] != nil || params.single[":rownames"] != nil { |
| 615 | | throw Failure.message(":cache with Lisp header values, :colnames or :rownames isn't supported yet; nothing was run.") |
| 717 | static func cacheInput(language: String, params: Params, tables: Tables, expanded: String, call: Bool) throws -> String { |
| 718 | if !params.lisp.isEmpty { |
| 719 | throw Failure.message(":cache with Lisp header values isn't supported yet; nothing was run.") |
| 616 | 720 | } |
| 617 | 721 | let ignored: Set<String> = ["replace", "silent", "none", "discard", "append", "prepend"] |
| 618 | 722 | let drawsFile = language == "dot" || language == "plantuml" |
| @@ -640,7 +744,10 @@ public enum Babel { |
| 640 | 744 | entries.append((":result-type", words.contains("output") ? "output" : "value")) |
| 641 | 745 | entries.append((":results", lispString(kept.joined(separator: " ")))) |
| 642 | 746 | if let wrap = params.wrap, !wrap.isEmpty { entries.append((":wrap", lispString(wrap))) } |
| 643 | | for (name, value) in vars { |
| 747 | for (key, names) in [(":colname-names", tables.colnames), (":rowname-names", tables.rownames)] where !names.isEmpty { |
| 748 | entries.append((key, "(" + names.map { "(\($0.name) " + lispLiteral(.list($0.names)).dropFirst() }.joined(separator: " ") + ")")) |
| 749 | } |
| 750 | for (name, value) in tables.vars { |
| 644 | 751 | switch value { |
| 645 | 752 | case .scalar, .number: entries.append((":var", "(\(name) . \(lispLiteral(value)))")) |
| 646 | 753 | case .list, .table: entries.append((":var", "(\(name) " + lispLiteral(value).dropFirst())) |
| @@ -650,8 +757,8 @@ public enum Babel { |
| 650 | 757 | return sorted.joined(separator: ":") + "-" + expanded |
| 651 | 758 | } |
| 652 | 759 | |
| 653 | | /// `org-babel-read-result` for fixed-width lines, a table without rules or a plain list after |
| 654 | | /// the `#+RESULTS` line at `line`; nil for anything else. |
| 760 | /// `org-babel-read-result` for fixed-width lines, a table or a plain list after the |
| 761 | /// `#+RESULTS` line at `line`; nil for anything else. |
| 655 | 762 | static func readResult(after line: Int, text ns: NSString) -> VarValue? { |
| 656 | 763 | var location = NSMaxRange(ns.lineRange(for: NSRange(location: line, length: 0))) |
| 657 | 764 | var lines: [String] = [] |
| @@ -673,10 +780,13 @@ public enum Babel { |
| 673 | 780 | return number(value).map(VarValue.number) ?? .scalar(value) |
| 674 | 781 | } |
| 675 | 782 | if first.hasPrefix("|") { |
| 676 | | var rows: [[Cell]] = [] |
| 783 | var rows: [[Cell]?] = [] |
| 677 | 784 | for l in lines { |
| 678 | 785 | guard l.hasPrefix("|") else { break } |
| 679 | | if l.hasPrefix("|-") { return nil } |
| 786 | if l.hasPrefix("|-") { |
| 787 | rows.append(nil) |
| 788 | continue |
| 789 | } |
| 680 | 790 | let cells = l.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) } |
| 681 | 791 | rows.append((l.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell)) |
| 682 | 792 | } |
| @@ -877,14 +987,15 @@ public enum Babel { |
| 877 | 987 | switch value(finish, output) { |
| 878 | 988 | case .none: return .scalar("") |
| 879 | 989 | case .string(let s): return number(s).map(VarValue.number) ?? .scalar(s) |
| 880 | | case .table(let rows): return .table(rows.compactMap { $0?.map(cell) }) |
| 990 | case .table(let rows): return .table(rows.map { $0?.map(cell) }) |
| 881 | 991 | } |
| 882 | 992 | } |
| 883 | 993 | |
| 884 | 994 | enum VarValue: Equatable, Sendable { |
| 885 | 995 | case scalar(String) |
| 886 | 996 | case number(String) |
| 887 | | case table([[Cell]]) |
| 997 | /// Rows; nil is a rule (`hline`). |
| 998 | case table([[Cell]?]) |
| 888 | 999 | case list([Cell]) |
| 889 | 1000 | } |
| 890 | 1001 | |
| @@ -949,6 +1060,7 @@ public enum Babel { |
| 949 | 1060 | static func resolve(_ value: String, model: DocumentModel, text: NSString, refs: References) throws -> VarValue { |
| 950 | 1061 | if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(Params.unescape(String(value.dropFirst().dropLast()))) } |
| 951 | 1062 | if let n = number(value) { return .number(n) } |
| 1063 | if let first = value.first, "('`[".contains(first) { return try lispValue(value) } |
| 952 | 1064 | if let known = refs.resolved[value] { return known } |
| 953 | 1065 | if let (block, arguments) = referencedBlock(value, model: model), let name = block.name { |
| 954 | 1066 | guard !refs.visiting.contains(name) else { throw Failure.message("\(name) refers to itself through :var") } |
| @@ -959,9 +1071,9 @@ public enum Babel { |
| 959 | 1071 | // A cached block gives its result without running, as `org-babel-execute-src-block` does. |
| 960 | 1072 | var useCache = true |
| 961 | 1073 | if params.single[":cache"] == "yes", let language = block.language { |
| 962 | | let vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) } |
| 1074 | let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params) |
| 963 | 1075 | let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block) |
| 964 | | let state = try cacheState(language: language, params: params, vars: vars, body: body, location: block.range, name: name, call: false, text: text) |
| 1076 | let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text) |
| 965 | 1077 | if state.current { |
| 966 | 1078 | if let value = state.value { return value } |
| 967 | 1079 | useCache = false |
| @@ -982,14 +1094,18 @@ public enum Babel { |
| 982 | 1094 | } |
| 983 | 1095 | let start = NSMaxRange(text.lineRange(for: NSRange(r, in: text as String))) |
| 984 | 1096 | var location = start |
| 985 | | var rows: [[Cell]] = [] |
| 1097 | var rows: [[Cell]?] = [] |
| 986 | 1098 | var items: [Cell] = [] |
| 987 | 1099 | while location < text.length { |
| 988 | 1100 | let lineRange = text.lineRange(for: NSRange(location: location, length: 0)) |
| 989 | 1101 | let line = text.substring(with: lineRange).trimmingCharacters(in: .newlines) |
| 990 | 1102 | let trimmed = line.trimmingCharacters(in: .whitespaces) |
| 991 | 1103 | if trimmed.hasPrefix("|") { |
| 992 | | if trimmed.hasPrefix("|-") { throw Failure.message("Tables with rules in :var aren't supported yet.") } |
| 1104 | if trimmed.hasPrefix("|-") { |
| 1105 | rows.append(nil) |
| 1106 | location = NSMaxRange(lineRange) |
| 1107 | continue |
| 1108 | } |
| 993 | 1109 | let cells = trimmed.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) } |
| 994 | 1110 | rows.append((trimmed.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell)) |
| 995 | 1111 | } else if let m = trimmed.firstMatch(of: /^[-+*] (.*)$/), rows.isEmpty { |
| @@ -1021,7 +1137,7 @@ public enum Babel { |
| 1021 | 1137 | switch value { |
| 1022 | 1138 | case .scalar(let s), .number(let s): return s |
| 1023 | 1139 | case .list(let items): return items.map(cell).joined(separator: "\n") |
| 1024 | | case .table(let rows): return rows.map { $0.map(cell).joined(separator: "\t") }.joined(separator: "\n") |
| 1140 | case .table(let rows): return rows.compactMap { $0?.map(cell).joined(separator: "\t") }.joined(separator: "\n") |
| 1025 | 1141 | } |
| 1026 | 1142 | } |
| 1027 | 1143 | |
| @@ -1037,7 +1153,7 @@ public enum Babel { |
| 1037 | 1153 | case .scalar(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r")) |
| 1038 | 1154 | case .number(let n): return n |
| 1039 | 1155 | case .list(let items): return "[" + items.map(cell).joined(separator: ", ") + "]" |
| 1040 | | case .table(let rows): return "[" + rows.map { "[" + $0.map(cell).joined(separator: ", ") + "]" }.joined(separator: ", ") + "]" |
| 1156 | case .table(let rows): return "[" + rows.map { $0.map { "[" + $0.map(cell).joined(separator: ", ") + "]" } ?? "None" }.joined(separator: ", ") + "]" |
| 1041 | 1157 | } |
| 1042 | 1158 | } |
| 1043 | 1159 | |
| @@ -1058,7 +1174,7 @@ public enum Babel { |
| 1058 | 1174 | case .scalar(let s): return lispString(s) |
| 1059 | 1175 | case .number(let n): return n |
| 1060 | 1176 | case .list(let items): return "(" + items.map(cell).joined(separator: " ") + ")" |
| 1061 | | case .table(let rows): return "(" + rows.map { "(" + $0.map(cell).joined(separator: " ") + ")" }.joined(separator: " ") + ")" |
| 1177 | case .table(let rows): return "(" + rows.map { $0.map { "(" + $0.map(cell).joined(separator: " ") + ")" } ?? "hline" }.joined(separator: " ") + ")" |
| 1062 | 1178 | } |
| 1063 | 1179 | } |
| 1064 | 1180 | |
| @@ -1229,6 +1345,10 @@ public enum Babel { |
| 1229 | 1345 | |
| 1230 | 1346 | /// The language's result handling: `org-babel-result-cond` and the table import. |
| 1231 | 1347 | static func value(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value { |
| 1348 | reassemble(rawValue(finish, output), finish) |
| 1349 | } |
| 1350 | |
| 1351 | static func rawValue(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value { |
| 1232 | 1352 | let params = finish.params |
| 1233 | 1353 | if params.contains("file") { |
| 1234 | 1354 | // `org-babel-result-to-file`: a link to `:file`, or to the result as a path. |