Babel differences from Emacs !165

merged merged by cmc on 2026-10-08 05:00 UTC · krz/orgstar:babel-differences into main

7 files changed, +642 −29

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +38 −23
@@ -190,7 +190,7 @@ public enum Babel {
190190 guard let m = line.firstMatch(of: #/(?i)^[ \t]*#\+CALL:[ \t]*([^\s(\[]+)(?:\[([^\]]*)\])?\((.*?)\)[ \t]*(.*?)[ \t]*\n?$/#) else { return nil }
191191 let name = String(m.1)
192192 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]
193 var extra = [lobDefaults] + callProperties(block, at: lineRange.location, model: model)
194194 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
195195 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
196196 if !m.4.isEmpty { extra.append(Params.parse(String(m.4))) }
@@ -216,13 +216,20 @@ public enum Babel {
216216 guard let object = found, let m = object.text.firstMatch(of: /^call_([^()\[\]\s]+)(?:\[([^\]]*)\])?\((.*?)\)(?:\[([^\]]*)\])?$/) else { return nil }
217217 let name = String(m.1)
218218 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]
219 var extra = [lobDefaults] + callProperties(block, at: object.range.lowerBound, model: model)
220220 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
221221 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
222222 if let end = m.4 { extra.append(Params.parse(String(end))) }
223223 return (block, try Params(block: block, model: model, text: text, extra: extra), object.range)
224224 }
225225
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
226233 /// `org-babel-default-lob-header-args`, over the called block's own header arguments.
227234 static let lobDefaults = [(":exports", "results")]
228235
@@ -246,7 +253,9 @@ public enum Babel {
246253 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil {
247254 call = (try? callAt(offset, model: model, text: text))?.params
248255 } else if tree.root.descendants().contains(where: { $0.kind == .inlineBabelCall && $0.range.lowerBound == offset }) {
249 call = (try? inlineCallAt(offset, tree: tree, model: model, text: text))?.params
256 // `org-babel-remove-result` only takes away `#+RESULTS:`, so an inline call's
257 // `{{{results(...)}}}` stays whatever `:exports` says.
258 return "results"
250259 } else if let inline = try? inlineAt(offset, tree: tree, model: model, text: text), inline.block.range.lowerBound == offset {
251260 return value(inline.params, language: inline.block.language)
252261 } else {
@@ -411,10 +420,11 @@ public enum Babel {
411420 let lines = shellAssignments(vars, shell: executable, params: params)
412421 let cmdline = params.single[":cmdline"]
413422 let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil }
414 if session == nil, stdin != nil || cmdline != nil || shebang != nil {
423 if stdin != nil || cmdline != nil || (session == nil && shebang != nil) {
415424 // `org-babel-sh-evaluate`: an executable script, run by the shell with the
416425 // command line after it and `:stdin` as its input; the result is its standard
417 // output, or its exit status for `:results value`.
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.
418428 var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n"
419429 if params.single[":padline"] != "no" { script += "\n" }
420430 script += (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
@@ -755,8 +765,9 @@ public enum Babel {
755765 call: Bool, text: NSString, options: Options) throws -> (hash: String, current: Bool, value: VarValue?) {
756766 // The hash is taken outside the shell's own binding of `shell-file-name`, so its
757767 // variables are written for the user's shell whatever the block's language.
758 let input = try cacheInput(language: language, params: params, tables: tables,
759 expanded: expandedBody(language: language, vars: tables.vars, body: body, shell: options.shell, params: params), call: call)
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)
760771 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
761772 guard let line = existingResults(name: name, block: location, text: text), resultsHash(at: line, text: text) == hash else {
762773 return (hash, false, nil)
@@ -922,22 +933,7 @@ public enum Babel {
922933 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [],
923934 inline: Bool = false) throws {
924935 var layers: [[(String, String)]] = [defaults]
925 let language = block.language ?? ""
926 // `org-babel-params-from-properties`: the inherited `header-args`, then the inherited
927 // `header-args:LANG`, each the nearest value with `+` additions after it.
928 var levels = [model.fileProperties]
929 if let heading = block.heading {
930 levels += (model.ancestors(of: heading).reversed() + [heading]).map { model.headings[$0].properties }
931 }
932 for key in ["header-args", "header-args:\(language)"] {
933 var value: String?
934 for entries in levels {
935 for entry in entries where entry.key.lowercased() == key {
936 value = entry.additive ? [value, entry.value].compactMap { $0 }.joined(separator: " ") : entry.value
937 }
938 }
939 if let value { layers.append(Params.parse(value)) }
940 }
936 layers += Params.propertyLayers(language: block.language ?? "", heading: block.heading, model: model)
941937 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
942938 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
943939 var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
@@ -991,6 +987,25 @@ public enum Babel {
991987 }
992988 }
993989
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
9941009 /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and
9951010 /// parentheses.
9961011 /// A Lisp string's contents as `read` gives them.
Sources/OrgCore/Compute/BabelExpand.swift added +401
@@ -0,0 +1,401 @@
1import Foundation
2
3// `org-babel-expand-body:LANG` of ob-C (C, C++, cpp, D), ob-java, ob-fortran and ob-clojure
4// (Org 9.8.7), which tangling and the `:cache` hash use. These languages don't run.
5
6extension Babel {
7 static let expandedLanguages: Set<String> = ["C", "C++", "cpp", "D", "java", "fortran", "clojure"]
8
9 /// The body as the language's `org-babel-expand-body:` writes it, or nil for other languages.
10 static func languageExpansion(_ body: String, language: String, tables: Tables, params: Params) throws -> String? {
11 switch language {
12 case "C", "C++", "cpp": return try expandC(body, d: false, tables: tables, params: params)
13 case "D": return try expandC(body, d: true, tables: tables, params: params)
14 case "java": return try expandJava(body, vars: tables.vars, params: params)
15 case "fortran": return try expandFortran(body, vars: tables.vars, params: params)
16 case "clojure": return expandClojure(body, vars: tables.vars, params: params)
17 default: return nil
18 }
19 }
20
21 enum HeaderValue {
22 case string(String)
23 case items([String])
24
25 /// `split-string` of a string, or the items.
26 var words: [String] {
27 switch self {
28 case .string(let s): s.split(whereSeparator: \.isWhitespace).map(String.init)
29 case .items(let items): items
30 }
31 }
32 }
33
34 /// A header value as `org-babel-read` gives it: a string, or the items of a Lisp list;
35 /// nil when absent.
36 static func headerValue(_ key: String, _ params: Params) throws -> HeaderValue? {
37 guard let raw = params.single[key] else { return nil }
38 var value = params.evaluated[key]
39 if value == nil, params.lisp.contains(key) {
40 guard let form = try? LispReader.readFirst(raw).sexp, let result = try? Elisp().eval(form) else {
41 throw Failure.message("\(key) \(raw) is Lisp that only Emacs can evaluate.")
42 }
43 value = result
44 }
45 guard let value else { return .string(raw) }
46 if let s = value.string { return .string(s) }
47 if Elisp.isNil(value) { return .items([]) }
48 guard case .list(let items) = value else { throw Failure.message("\(key) \(raw) isn't a list of names.") }
49 return .items(items.map { $0.string ?? Elisp.printed($0) })
50 }
51
52 enum BaseType { case integer, float, string }
53
54 /// `org-babel-C-val-to-base-type` and `org-babel-java-val-to-base-type`.
55 static func baseType(_ cells: [Cell]) -> BaseType? {
56 var type: BaseType?
57 for cell in cells {
58 switch cell {
59 case .text: type = .string
60 case .number(let n) where n.contains(".") || n.contains("e") || n.contains("N"):
61 if type != .string { type = .float }
62 case .number:
63 if type == nil { type = .integer }
64 }
65 }
66 return type
67 }
68
69 static func cells(_ value: VarValue) throws -> [Cell] {
70 switch value {
71 case .scalar(let s): return [.text(s)]
72 case .number(let n): return [.number(n)]
73 case .list(let items): return items
74 case .table(let rows):
75 guard !rows.contains(where: { $0 == nil }) else { throw Failure.message("Tables with rules in :var aren't supported for this language yet.") }
76 return rows.flatMap { $0! }
77 }
78 }
79
80 // MARK: - C, C++ and D
81
82 static let cColumnUtility = """
83
84 #ifndef _STRING_H
85 #include <string.h>
86 #endif
87 int get_column_num (int nbcols, const char** header, const char* column)
88 {
89 int c;
90 for (c=0; c<nbcols; c++)
91 if (strcmp(header[c],column)==0)
92 return c;
93 return -1;
94 }
95
96 """
97 static let dColumnUtility = """
98 int get_column_num (string[] header, string column)
99 {
100 foreach (c, h; header)
101 if (h==column)
102 return to!int(c);
103 return -1;
104 }
105
106 """
107
108 /// `org-babel-C-expand-C` and `org-babel-C-expand-D`.
109 static func expandC(_ body: String, d: Bool, tables: Tables, params: Params) throws -> String {
110 let vars = tables.vars
111 func typeName(_ type: BaseType?) throws -> String {
112 switch type {
113 case .integer: return "int"
114 case .float: return "double"
115 case .string: return d ? "string" : "const char*"
116 case nil: throw Failure.message("A :var value has no type C can take.")
117 }
118 }
119 func literal(_ cell: Cell, _ type: BaseType) -> String {
120 type == .string ? "\"" + cellText(cell) + "\"" : cellText(cell)
121 }
122 // `org-babel-C-var-to-C`.
123 let declarations = try vars.map { name, value -> String in
124 let type = try baseType(cells(value))
125 let typeName = try typeName(type)
126 var suffix = ""
127 let data: String
128 switch value {
129 case .scalar, .number:
130 data = literal(try cells(value)[0], type!)
131 case .list(let items):
132 suffix = "[\(items.count)]"
133 data = (d ? "[" : "{") + items.map { literal($0, type!) }.joined(separator: ",") + (d ? "]" : "}")
134 case .table(let rows):
135 let rows = rows.map { $0! }
136 suffix = d ? "[\(rows.first?.count ?? 0)][\(rows.count)]" : "[\(rows.count)][\(rows.first?.count ?? 0)]"
137 data = (d ? "[\n" : "{\n")
138 + rows.map { (d ? " [" : " {") + $0.map { literal($0, type!) }.joined(separator: ",") + (d ? "]" : "}") }.joined(separator: ",\n")
139 + (d ? "\n]" : "\n}")
140 }
141 return d ? "\(typeName)\(suffix) \(name) = \(data);" : "\(typeName) \(name)\(suffix) = \(data);"
142 }
143 // `org-babel-C-table-sizes-to-C`.
144 let sizes = vars.map { name, value -> String in
145 switch value {
146 case .table(let rows): "const int \(name)_rows = \(rows.count);\nconst int \(name)_cols = \(rows.first??.count ?? 0);"
147 case .list(let items): "const int \(name)_cols = \(items.count);"
148 case .scalar, .number: ""
149 }
150 }
151 // `org-babel-C-header-to-C`.
152 let headers = try tables.colnames.map { table, names -> String in
153 let value = vars.first { $0.0 == table }?.1
154 let typeName = try typeName(try value.map(cells).flatMap(baseType))
155 let quoted = names.map { "\"" + cellText($0) + "\"" }.joined(separator: ",")
156 return d
157 ? "string[\(names.count)] \(table)_header = [\(quoted)];\n\(typeName) \(table)_h (size_t row, string col) { return \(table)[row][get_column_num(\(table)_header,col)]; }"
158 : "const char* \(table)_header[\(names.count)] = {\(quoted)};\n\(typeName) \(table)_h (int row, const char* col) { return \(table)[row][get_column_num(\(names.count),\(table)_header,col)]; }"
159 }
160 var body = body
161 var parts: [String] = []
162 if d {
163 // `:imports` and the inherited IMPORTS property, which tangling reads outside the
164 // Org buffer, so never.
165 parts.append("module mmm;")
166 parts.append(((try headerValue(":imports", params)?.words ?? []) + ["std.stdio", "std.conv"]).map { "import \($0);" }.joined(separator: "\n"))
167 } else {
168 let includes = try headerValue(":includes", params)?.words ?? []
169 parts.append(includes.map { $0.hasPrefix("<") ? "#include \($0)" : "#include \"\($0)\"" }.joined(separator: "\n"))
170 var defines: [String] = []
171 switch try headerValue(":defines", params) {
172 case .string(let s):
173 let words = HeaderValue.string(s).words
174 defines = stride(from: 0, to: words.count - 1, by: 2).map { words[$0] + " " + words[$0 + 1] }
175 case .items(let items): defines = items
176 case nil: break
177 }
178 parts.append(defines.map { "#define \($0)" }.joined(separator: "\n"))
179 parts.append((try headerValue(":namespaces", params)?.words ?? []).map { "using namespace \($0);" }.joined(separator: "\n"))
180 body = (params.single[":prologue"].map { $0 + "\n" } ?? "") + body + (params.single[":epilogue"].map { "\n" + $0 + "\n" } ?? "")
181 }
182 parts.append(declarations.joined(separator: "\n"))
183 parts.append(sizes.joined(separator: "\n"))
184 parts.append(tables.colnames.isEmpty ? "" : d ? dColumnUtility : cColumnUtility)
185 parts.append(headers.joined(separator: "\n"))
186 // `org-babel-C-ensure-main-wrap`.
187 let hasMain = body.range(of: "(?im)^[ \\t]*[intvod]+[ \\t\\n\\r]*main[ \\t]*\\(.*\\)", options: .regularExpression) != nil
188 parts.append(params.single[":main"] == "no" || hasMain ? body : "int main() {\n\(body)\nreturn 0;\n}\n")
189 parts.append("\n")
190 return parts.joined(separator: "\n")
191 }
192
193 // MARK: - Java
194
195 static let javaSpace = "[ \\t\\n\\r\\f]"
196 static let javaPackage = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*package\(javaSpace)+([\\p{L}\\p{N}_.]+)\(javaSpace)*;$")
197 static let javaImports = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*import(?:\(javaSpace)+static)?\(javaSpace)+([\\p{L}\\p{N}_.*]+)\(javaSpace)*;$")
198 static let javaClass = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*(?:public\(javaSpace)+)?class\(javaSpace)+([\\p{L}\\p{N}_]+)\(javaSpace)*\\{")
199 static let javaMain = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*public\(javaSpace)+static\(javaSpace)+void\(javaSpace)+main\(javaSpace)*\\(\(javaSpace)*String"
200 + "[\\p{L}\\p{N}_\\[\\] \\t\\n\\r\\f]+\\)\(javaSpace)*(?:throws[\\p{L}\\p{N}_,. \\t\\n\\r\\f]+)?\\{")
201 static let javaMethod = try! NSRegularExpression(pattern: "(?im)^\(javaSpace)*(?:[\\p{L}\\p{N}]+\(javaSpace)+)?(?:static\(javaSpace)+)?[\\p{L}\\p{N}_\\[\\]]+\(javaSpace)+[\\p{L}\\p{N}_]+"
202 + "\(javaSpace)*\\([\\p{L}\\p{N}_\\[\\], \\t\\n\\r\\f]*\\)\(javaSpace)*(?:throws[\\p{L}\\p{N}_,. \\t\\n\\r\\f]+)?\\{")
203
204 /// `org-babel-expand-body:java`: the body wrapped in a `main` method when it has no method,
205 /// and in a class when it has none, with `:var` as static fields, `:imports` and the
206 /// package of `:classname`.
207 static func expandJava(_ body: String, vars: [(String, VarValue)], params: Params) throws -> String {
208 let fullName = params.single[":classname"] ?? javaClassName(body)
209 let className = fullName.components(separatedBy: ".").last!
210 let packageName = fullName.contains(".") ? (fullName as NSString).deletingPathExtension : nil
211 let varLines = try vars.map { name, value -> String in
212 // A rule is `hline`, a string.
213 let typed: [Cell]
214 if case .table(let rows) = value, rows.contains(where: { $0 == nil }) {
215 typed = rows.compactMap { $0 }.flatMap { $0 } + [.text("hline")]
216 } else {
217 typed = try cells(value)
218 }
219 guard let type = baseType(typed) else { throw Failure.message("A :var value has no type java can take.") }
220 let base = type == .integer ? "Integer" : type == .float ? "Double" : "String"
221 func literal(_ cell: Cell?) throws -> String {
222 guard let cell else { return "null" }
223 switch type {
224 case .integer: return cellText(cell)
225 case .float: return String(format: "%f", Double(cellText(cell)) ?? 0)
226 case .string:
227 let s = cellText(cell)
228 if s.range(of: ".\\n+.", options: .regularExpression) != nil { throw Failure.message("Java does not support multiline string literals") }
229 return "\"" + s + "\""
230 }
231 }
232 let declared: String
233 let data: String
234 switch value {
235 case .scalar, .number:
236 declared = base
237 data = try literal(try cells(value)[0])
238 case .list(let items):
239 declared = "List<\(base)>"
240 data = "Arrays.asList(" + (try items.map { try literal($0) }).joined(separator: ", ") + ")"
241 case .table(let rows):
242 declared = "List<List<\(base)>>"
243 data = "Arrays.asList(" + (try rows.map { row in
244 try row.map { "Arrays.asList(" + (try $0.map { try literal($0) }).joined(separator: ", ") + ")" } ?? "null"
245 }).joined(separator: ", ") + ")"
246 }
247 return " static \(declared) \(name) = \(data);"
248 }
249 let imports = try headerValue(":imports", params).map { value -> [String] in
250 guard case .string(let s) = value else { throw Failure.message(":imports for java takes a string.") }
251 return s.components(separatedBy: " ")
252 }
253
254 let buffer = NSMutableString(string: (params.single[":prologue"].map { $0 + "\n" } ?? "") + body + (params.single[":epilogue"].map { "\n" + $0 } ?? ""))
255 func search(_ regex: NSRegularExpression, from point: Int) -> NSTextCheckingResult? {
256 regex.firstMatch(in: buffer as String, options: [.withTransparentBounds, .withoutAnchoringBounds],
257 range: NSRange(location: point, length: buffer.length - point))
258 }
259 // `org-babel-java--move-past`.
260 func movePast(_ regex: NSRegularExpression, _ point: inout Int) {
261 while let m = search(regex, from: point) { point = min(NSMaxRange(m.range) + 1, buffer.length) }
262 }
263 func wrap(_ opening: String) {
264 var point = 0
265 movePast(javaPackage, &point)
266 movePast(javaImports, &point)
267 buffer.insert(opening, at: point)
268 indentRigidly(buffer, from: point + opening.utf16.count, by: 4)
269 buffer.append("\n}")
270 }
271 if search(javaMain, from: 0) == nil, search(javaMethod, from: 0) == nil { wrap("public static void main(String[] args) {\n") }
272 if search(javaClass, from: 0) == nil { wrap("\npublic class \(className) {\n") }
273 if !varLines.isEmpty {
274 var point = 0
275 movePast(javaClass, &point)
276 buffer.insert(varLines.joined(separator: "\n") + "\n", at: point)
277 }
278 if let imports {
279 var point = 0
280 movePast(javaPackage, &point)
281 buffer.insert(imports.map { "import \($0);" }.joined(separator: "\n") + "\n", at: point)
282 }
283 if let packageName, search(javaPackage, from: 0) == nil { buffer.insert("package \(packageName);\n", at: 0) }
284 return buffer as String
285 }
286
287 /// `org-babel-java-find-classname`.
288 static func javaClassName(_ body: String) -> String {
289 let ns = body as NSString
290 func first(_ regex: NSRegularExpression) -> String? {
291 regex.firstMatch(in: body, range: NSRange(location: 0, length: ns.length)).map { ns.substring(with: $0.range(at: 1)) }
292 }
293 let package = first(javaPackage)
294 let className = first(javaClass)
295 if let package, let className { return package + "." + className }
296 return className ?? package.map { $0 + ".Main" } ?? "Main"
297 }
298
299 /// `indent-code-rigidly` from `start`, at the beginning of a line, to the end: each line
300 /// not starting inside a string moved right, blank lines emptied, with `indent-tabs-mode` nil.
301 static func indentRigidly(_ buffer: NSMutableString, from start: Int, by columns: Int) {
302 var location = start
303 var inString = false
304 while location < buffer.length {
305 if !inString {
306 var end = location
307 var column = 0
308 while end < buffer.length, let c = UnicodeScalar(buffer.character(at: end)), c == " " || c == "\t" {
309 column = c == "\t" ? (column / 8 + 1) * 8 : column + 1
310 end += 1
311 }
312 let atEnd = end >= buffer.length || buffer.character(at: end) == 10
313 let indent = atEnd ? "" : String(repeating: " ", count: column + columns)
314 buffer.replaceCharacters(in: NSRange(location: location, length: end - location), with: indent)
315 }
316 let line = buffer.lineRange(for: NSRange(location: location, length: 0))
317 var escaped = false
318 for i in line.location..<NSMaxRange(line) {
319 let c = buffer.character(at: i)
320 if escaped { escaped = false } else if c == 92 { escaped = true } else if c == 34 { inString.toggle() }
321 }
322 location = NSMaxRange(line)
323 }
324 }
325
326 // MARK: - Fortran
327
328 /// `org-babel-expand-body:fortran`.
329 static func expandFortran(_ body: String, vars: [(String, VarValue)], params: Params) throws -> String {
330 let prologue = params.single[":prologue"]
331 let epilogue = params.single[":epilogue"]
332 // `%S`.
333 func printed(_ cell: Cell) -> String {
334 switch cell {
335 case .number(let n): n
336 case .text(let s): lispString(s)
337 }
338 }
339 // `org-babel-fortran-var-to-fortran`.
340 let declarations = try vars.map { name, value -> String in
341 switch value {
342 case .number(let n) where baseType([.number(n)]) == .integer: return "integer, parameter :: \(name) = \(n)\n"
343 case .number(let n): return "real, parameter :: \(name) = \(n)\n"
344 case .scalar(let s): return "character(len=\(s.unicodeScalars.count)), parameter :: \(name) = '\(s)'\n"
345 case .table(let rows):
346 guard !rows.contains(where: { $0 == nil }) else { throw Failure.message("Tables with rules in :var aren't supported for fortran yet.") }
347 let rows = rows.map { $0! }
348 let columns = rows.first?.count ?? 0
349 let data = "(/" + rows.map { "(/" + $0.map(printed).joined(separator: ", ") + "/)" }.joined(separator: ", ") + "/)"
350 return "real, parameter :: \(name)(\(rows.count),\(columns)) = transpose( reshape( \(data) , (/ \(columns), \(rows.count) /) ) )\n"
351 case .list(let items):
352 return "real, parameter :: \(name)(\(items.count)) = (/" + items.map(printed).joined(separator: ", ") + "/)\n"
353 }
354 }
355 // The INCLUDES and DEFINES properties are read outside the Org buffer when tangling.
356 func items(_ value: HeaderValue?) -> [String] {
357 switch value {
358 case .string(let s): [s]
359 case .items(let items): items
360 case nil: []
361 }
362 }
363 let includes = items(try headerValue(":includes", params))
364 let defines = items(try headerValue(":defines", params))
365 let main: String
366 if params.single[":main"] == "no" {
367 main = (prologue.map { $0 + "\n" } ?? "") + body + (epilogue.map { "\n" + $0 + "\n" } ?? "")
368 } else {
369 // Org writes the prologue again where the epilogue would go.
370 let inner = declarations.joined(separator: "\n") + (prologue.map { $0 + "\n" } ?? "") + body + (prologue.map { $0 + "\n" } ?? "")
371 if inner.range(of: "(?im)^[ \\t]*program\\b", options: .regularExpression) != nil {
372 if !vars.isEmpty { throw Failure.message("Cannot use :vars if `program' statement is present") }
373 main = inner
374 } else {
375 main = "program main\n\(inner)\nend program main\n"
376 }
377 }
378 return [includes.map { "#include \($0)" }.joined(separator: "\n"), defines.map { "#define \($0)" }.joined(separator: "\n"), main, "\n"]
379 .joined(separator: "\n")
380 }
381
382 // MARK: - Clojure
383
384 /// `org-babel-expand-body:clojure`, for a backend other than CIDER's (whose namespace
385 /// only matters with `:ns`, which is then used).
386 static func expandClojure(_ body: String, vars: [(String, VarValue)], params: Params) -> String {
387 var expanded = params.single[":ns"].map { "(ns \($0))\n" } ?? ""
388 if vars.isEmpty {
389 expanded += body.trimmingCharacters(in: .whitespacesAndNewlines)
390 } else {
391 let uncommented = body.replacingOccurrences(of: "(?m)^[ ]*;+.*$", with: "", options: .regularExpression)
392 expanded += "(let [" + vars.map { "\($0.0) '\(lispLiteral($0.1))" }.joined(separator: "\n ") + "]\n\(uncommented))"
393 }
394 expanded = expanded.trimmingCharacters(in: .whitespacesAndNewlines)
395 if params.results.contains("output") { return expanded }
396 let printer = params.results.contains("code") || params.results.contains("pp")
397 ? "(require '[clojure.pprint :refer [pprint]]) (pprint "
398 : "(prn "
399 return printer + "(binding [*out* (java.io.StringWriter.)]" + expanded + "))"
400 }
401}
Sources/OrgCore/Compute/Tangle.swift +7 −4
@@ -133,7 +133,8 @@ public enum Tangle {
133133
134134 static func params(_ block: SrcBlockInfo, model: DocumentModel, text: NSString, path: String) throws -> Babel.Params {
135135 var params = try Babel.Params(block: block, model: model, text: text)
136 for key in params.lisp where key != ":tangle-mode" {
136 // Lists for ob-C, ob-java and ob-fortran are read by the language's expansion.
137 for key in params.lisp where ![":tangle-mode", ":includes", ":defines", ":namespaces", ":imports"].contains(key) {
137138 guard let value = params.single[key] else { continue }
138139 params.single[key] = try evaluate(value, key: key, path: path)
139140 }
@@ -357,16 +358,18 @@ public enum Tangle {
357358 }
358359
359360 /// `org-babel-expand-body:LANG`, or the generic expansion with the language's variable
360 /// assignments.
361 /// assignments; tables in `:var` taken apart by `org-babel-disassemble-tables`.
361362 static func expandBody(_ body: String, language: String, params: Babel.Params, model: DocumentModel, text: NSString) throws -> String {
362363 let refs = Babel.References(resolved: [:], options: Babel.Options())
363 let vars = try params.vars.map { variable in
364 let tables = try Babel.disassemble(try params.vars.map { variable in
364365 do {
365366 return (variable.name, try Babel.resolve(variable.value, model: model, text: text, refs: refs))
366367 } catch is Babel.Needed {
367368 throw Babel.Failure.message(":var \(variable.value) would run a block while tangling, which isn't supported yet; nothing was tangled.")
368369 }
369 }
370 }, params: params)
371 let vars = tables.vars
372 if let expanded = try Babel.languageExpansion(body, language: language, tables: tables, params: params) { return expanded }
370373 let prologue = params.single[":prologue"]
371374 let epilogue = params.single[":epilogue"]
372375 if language == "emacs-lisp" || language == "elisp" {
Tests/OrgCoreTests/BabelCacheTests.swift +27
@@ -121,6 +121,33 @@ struct BabelCacheTests {
121121 #expect(job.directory == (emacs[0] as NSString).deletingLastPathComponent + "/")
122122 }
123123
124 /// Languages that don't run hash the body as their `org-babel-expand-body:LANG` writes it.
125 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
126 func expandedLanguagesHashLikeEmacs() throws {
127 let texts = [
128 "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | 2 |\n\n#+begin_src C :cache yes :includes <stdio.h> :defines N 1 :var t=tbl n=2\nprintf(\"%d\", n);\n#+end_src\n",
129 "#+begin_src C++ :cache yes :namespaces std :main no\nint f();\n#+end_src\n",
130 "#+begin_src D :cache yes :var s=\"x\"\nwriteln(s);\n#+end_src\n",
131 "#+begin_src fortran :cache yes :var n=3\nprint *, n\n#+end_src\n",
132 "#+begin_src clojure :cache yes :var n=3\n(+ n 1)\n#+end_src\n",
133 ]
134 for text in texts {
135 let emacs = try EmacsOracle.evaluate(text, """
136 (progn (require 'ob-C) (require 'ob-fortran) (require 'ob-clojure) (setq org-babel-clojure-backend 'babashka)
137 (goto-char (point-min)) (search-forward "#+begin_src")
138 (list (org-babel-sha1-hash)))
139 """)
140 let cached = text + "\n#+RESULTS[\(emacs[0])]:\n: 42\n"
141 let caret = (cached as NSString).range(of: "#+begin_src").location + 2
142 let context = EditContext(revision: 0, text: cached, tree: OrgParser.parse(cached), selection: [caret..<caret])
143 guard case .failure(.message(let message)) = Babel.plan(context) else {
144 Issue.record("planned:\n\(text)")
145 continue
146 }
147 #expect(message == "Cached: 42", "\(text)")
148 }
149 }
150
124151 @Test func lispHeadersAreRefused() {
125152 let text = "#+begin_src sh :cache yes :dir (user-full-name)\necho hi\n#+end_src\n"
126153 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
Tests/OrgCoreTests/BabelTests.swift +26
@@ -156,6 +156,10 @@ struct BabelTests {
156156 "#+begin_src sh :results value :cmdline x\necho one\n#+end_src\n",
157157 "#+begin_src bash :shebang #!/bin/bash :cmdline \"a b\" c :results output\necho $#\n#+end_src\n",
158158 "#+begin_src sh :stdin src :results output\ntr a-z A-Z\n#+end_src\n\n#+NAME: src\n#+begin_src sh\necho hello\n#+end_src\n",
159 // `:stdin` and `:cmdline` run a script rather than the session.
160 "#+begin_src sh :session s1 :cmdline a b :results output\necho $# $1\n#+end_src\n",
161 "#+NAME: in\n| hello |\n\n#+begin_src sh :session s2 :stdin in :results output\ncat\n#+end_src\n",
162 "#+begin_src bash :session s3 :shebang #!/bin/bash :cmdline x :var v=3\necho $v $1 ${BASH_VERSION:+bash}\n#+end_src\n",
159163 // `:var` from other blocks' results.
160164 "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh\necho 5\n#+end_src\n",
161165 "#+begin_src python :var t=tbl\nreturn [len(t), t[1][0]]\n#+end_src\n\n#+NAME: tbl\n#+begin_src python\nreturn [[1, 2], [3, 4]]\n#+end_src\n",
@@ -218,6 +222,8 @@ struct BabelTests {
218222 (setq org-confirm-babel-evaluate nil
219223 org-babel-python-command "python3"
220224 default-directory temporary-file-directory)
225 ;; No shell session in batch; the cases with one don't use it.
226 (advice-add 'org-babel-sh-initiate-session :override (lambda (s &rest _) (unless (member s '(nil "none")) s)))
221227 (org-babel-execute-src-block))
222228 """))
223229 }
@@ -362,7 +368,27 @@ struct BabelCallTests {
362368 (named + "Inline call_double[:results verbatim](n=1) here.\n", "call_"),
363369 (named + "Inline call_double(n=2) {{{results(=0=)}}} here.\n", "call_"),
364370 (named + "Inline call_double(n=2) and call_double(n=3) here.\n", "call_double(n=3"),
371 // Properties where the call is, not where the block is.
372 (underHeadings + "#+CALL: double()\n", "#+CALL"),
373 (underHeadings + "#+CALL: double() :var n=1\n", "#+CALL"),
374 (underHeadings + "Inline call_double() here.\n", "call_"),
365375 ]
376 static let underHeadings = """
377 * Library
378 :PROPERTIES:
379 :header-args: :results verbatim
380 :END:
381 #+NAME: double
382 #+begin_src sh :var n=2
383 echo $((n*2))
384 #+end_src
385 * Use
386 :PROPERTIES:
387 :header-args:sh: :var n=7
388 :header-args: :results raw
389 :END:
390
391 """
366392
367393 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
368394 func matchesEmacs() throws {
Tests/OrgCoreTests/ExportTests.swift +13
@@ -320,6 +320,19 @@ struct ExportResultsTests {
320320 Inline src_sh[:exports both]{echo codeinlineboth} {{{results(=resinlineboth=)}}} three.
321321
322322 Inline call_named() {{{results(=resinlinecall=)}}} four.
323
324 * Other
325 :PROPERTIES:
326 :header-args: :exports none
327 :END:
328 #+CALL: named()
329
330 #+RESULTS:
331 : rescallother
332
333 Inline call_named() {{{results(=resinlinecallother=)}}} five.
334
335 Inline call_named[:exports code]() {{{results(=resinlinecallcode=)}}} six.
323336 """ + "\n"
324337
325338 /// Which code and results the HTML and Markdown exports keep, against ox-html with nothing
Tests/OrgCoreTests/TangleTests.swift +130 −2
@@ -230,7 +230,7 @@ struct TangleTests {
230230 (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
231231 (src (expand-file-name "notes.org" dir))
232232 (text (buffer-string)) out)
233 (require 'ob-shell) (require 'ob-python)
233 (require 'ob-shell) (require 'ob-python) (require 'ob-C) (require 'ob-java)
234234 (with-temp-file src (insert text))
235235 (with-current-buffer (find-file-noselect src)
236236 (let ((files (org-babel-tangle)))
@@ -291,7 +291,7 @@ struct TangleTests {
291291 #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
292292 }
293293
294 /// The file names only: Emacs also wraps C, D, Java, Fortran and Clojure bodies with org-babel-expand-body:LANG.
294 /// The file names only; `expandsBodiesLikeOrg` checks the bodies.
295295 @Test func extensionsLikeOrg() throws {
296296 let languages = ["python", "ruby", "perl", "C", "C++", "D", "awk", "sed", "lua", "haskell", "java", "latex", "LilyPond",
297297 "fortran", "clojure", "ocaml", "julia", "sh", "text"]
@@ -315,6 +315,134 @@ struct TangleTests {
315315 #expect(names == theirs.text, "ours:\n\(names)\nemacs:\n\(theirs.text)\n\(theirs.error)")
316316 }
317317
318 static let expanded = #"""
319 #+NAME: tbl
320 | a | b |
321 |---+-----|
322 | 1 | 2.5 |
323 | 3 | 4 |
324
325 #+NAME: items
326 - x
327 - y
328
329 #+NAME: ints
330 | 1 | 2 |
331 | 3 | 4 |
332
333 * C
334 :PROPERTIES:
335 :includes: <never.h>
336 :imports: never.d
337 :END:
338 #+begin_src C :tangle out.c
339 printf("hi\n");
340 #+end_src
341 #+begin_src C :tangle out.c :includes <stdio.h> <math.h> :defines N 5 M 6 :var n=3 x=2.5 s="str" l=items
342 printf("%d\n", n);
343 #+end_src
344 #+begin_src C :tangle out.c :includes '(<stdio.h> "local.h") :defines '("A 1") :var t=tbl
345 int main() { return t_h(0, "a"); }
346 #+end_src
347 #+begin_src C :tangle out.c :main no :prologue "// pro" :epilogue "// epi" :var m=ints
348 int f(void) { return 1; }
349 #+end_src
350 #+begin_src C++ :tangle out.cpp :namespaces std :includes <iostream>
351 cout << 1;
352 #+end_src
353 #+begin_src cpp :tangle out.cpp
354 INT main(void) {}
355 #+end_src
356 #+begin_src D :tangle out.d :imports std.math :var n=3 s="d" l=items t=tbl
357 writeln(n);
358 #+end_src
359 #+begin_src D :tangle out.d :main no
360 void f() {}
361 #+end_src
362 * Java
363 #+begin_src java :tangle Main.java
364 System.out.println(1);
365 if (true) {
366 System.out.println(2);
367 }
368 #+end_src
369 #+begin_src java :tangle Foo.java :classname com.x.Foo :imports java.util.List java.util.Map :var n=3 x=2.5 s="s" l=items t=ints
370 System.out.println(n);
371 #+end_src
372 #+begin_src java :tangle Pkg.java
373 package org.y;
374 import java.util.List;
375
376 public class Pkg {
377 public static void main(String[] args) {
378 System.out.println(1);
379 }
380 }
381 #+end_src
382 #+begin_src java :tangle Methods.java :var n=1
383 static int twice(int x) {
384 return 2 * x;
385 }
386 #+end_src
387 #+begin_src java :tangle Pro.java :prologue "int a = 1;" :epilogue "int b = 2;"
388 int c = 3;
389 #+end_src
390 * Fortran
391 #+begin_src fortran :tangle out.F90
392 print *, 1
393 #+end_src
394 #+begin_src fortran :tangle out.F90 :var n=3 x=2.5 s="str" l='(1 2) m=ints :includes "a.h" :defines N 5 :prologue "! pro"
395 print *, n
396 #+end_src
397 #+begin_src fortran :tangle out.F90
398 PROGRAM hello
399 print *, 2
400 end program hello
401 #+end_src
402 #+begin_src fortran :tangle out.F90 :main no :prologue "! pro" :epilogue "! epi" :var n=1
403 subroutine s
404 end subroutine s
405 #+end_src
406 * Clojure
407 #+begin_src clojure :tangle out.clj
408 (+ 1 2)
409 #+end_src
410 #+begin_src clojure :tangle out.clj :var n=3 s="a\"b" l=items t=tbl
411 ;; a comment
412 (+ n 1)
413 #+end_src
414 #+begin_src clojure :tangle out.clj :ns my.ns :results output
415 (println 1)
416 #+end_src
417 #+begin_src clojure :tangle out.clj :results pp
418 {:a 1}
419 #+end_src
420 * Python
421 #+begin_src python :tangle out.py :var t=tbl
422 print(t)
423 #+end_src
424 """# + "\n"
425
426 @Test func expandsBodiesLikeOrg() throws {
427 let form = """
428 (let* ((dir (file-truename (make-temp-file "orgstar-tangle" t)))
429 (src (expand-file-name "notes.org" dir))
430 (text (buffer-string)) out)
431 (require 'ob-C) (require 'ob-java) (require 'ob-fortran) (require 'ob-clojure) (require 'ob-python)
432 (setq org-babel-clojure-backend 'babashka)
433 (with-temp-file src (insert text))
434 (with-current-buffer (find-file-noselect src)
435 (let ((files (org-babel-tangle)))
436 (setq out (mapconcat (lambda (f) (format "== %s %o\\n%s" (file-relative-name f dir) (file-modes f)
437 (with-temp-buffer (insert-file-contents f) (buffer-string))))
438 (sort (copy-sequence files) #'string<) ""))))
439 (erase-buffer) (insert out))
440 """
441 let theirs = try EmacsOracle.run([EmacsOracle.Case(text: Self.expanded, point: 1, form: form)])[0]
442 let ours = try Tangle.run(text: Self.expanded, path: "/oracle/notes.org").get()
443 #expect(Self.describe(ours, root: "/oracle") == theirs.text, "ours:\n\(Self.describe(ours, root: "/oracle"))\nemacs:\n\(theirs.text)\n\(theirs.error)")
444 }
445
318446 @Test func refusesWhatItCantDo() {
319447 for (language, header) in [("haskell", ":comments link"), ("sh", ":tangle (concat user-emacs-directory \"a.sh\")"), ("sh", ":tangle-mode go+q")] {
320448 let text = "#+begin_src \(language) :tangle yes \(header)\necho\n#+end_src\n"