krz/orgstar

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

Commit d8a3718c32

d8a3718c3265dc2024016bdf179e00d633ce47ea

parent: 2794492110

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 04:56 UTC

Read header-args properties for calls where the call is

org-babel-lob-get-info merges the header-args properties at the #+CALL line or
inline call after org-babel-default-lob-header-args, for evaluation and :exports.
An inline call's {{{results(...)}}} stays in export whatever :exports says.

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +32 −19
@@ -190,7 +190,7 @@ public enum Babel {
190 guard let m = line.firstMatch(of: #/(?i)^[ \t]*#\+CALL:[ \t]*([^\s(\[]+)(?:\[([^\]]*)\])?\((.*?)\)[ \t]*(.*?)[ \t]*\n?$/#) else { return nil } 190 guard let m = line.firstMatch(of: #/(?i)^[ \t]*#\+CALL:[ \t]*([^\s(\[]+)(?:\[([^\]]*)\])?\((.*?)\)[ \t]*(.*?)[ \t]*\n?$/#) else { return nil }
191 let name = String(m.1) 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") } 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] 193 var extra = [lobDefaults] + callProperties(block, at: lineRange.location, model: model)
194 if let inside = m.2 { extra.append(Params.parse(String(inside))) } 194 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
195 extra.append(splitArguments(String(m.3)).map { (":var", $0) }) 195 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
196 if !m.4.isEmpty { extra.append(Params.parse(String(m.4))) } 196 if !m.4.isEmpty { extra.append(Params.parse(String(m.4))) }
@@ -216,13 +216,20 @@ public enum Babel {
216 guard let object = found, let m = object.text.firstMatch(of: /^call_([^()\[\]\s]+)(?:\[([^\]]*)\])?\((.*?)\)(?:\[([^\]]*)\])?$/) else { return nil } 216 guard let object = found, let m = object.text.firstMatch(of: /^call_([^()\[\]\s]+)(?:\[([^\]]*)\])?\((.*?)\)(?:\[([^\]]*)\])?$/) else { return nil }
217 let name = String(m.1) 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") } 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] 219 var extra = [lobDefaults] + callProperties(block, at: object.range.lowerBound, model: model)
220 if let inside = m.2 { extra.append(Params.parse(String(inside))) } 220 if let inside = m.2 { extra.append(Params.parse(String(inside))) }
221 extra.append(splitArguments(String(m.3)).map { (":var", $0) }) 221 extra.append(splitArguments(String(m.3)).map { (":var", $0) })
222 if let end = m.4 { extra.append(Params.parse(String(end))) } 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) 223 return (block, try Params(block: block, model: model, text: text, extra: extra), object.range)
224 } 224 }
225 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
226 /// `org-babel-default-lob-header-args`, over the called block's own header arguments. 233 /// `org-babel-default-lob-header-args`, over the called block's own header arguments.
227 static let lobDefaults = [(":exports", "results")] 234 static let lobDefaults = [(":exports", "results")]
228 235
@@ -246,7 +253,9 @@ public enum Babel {
246 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil { 253 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil {
247 call = (try? callAt(offset, model: model, text: text))?.params 254 call = (try? callAt(offset, model: model, text: text))?.params
248 } else if tree.root.descendants().contains(where: { $0.kind == .inlineBabelCall && $0.range.lowerBound == offset }) { 255 } 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"
250 } else if let inline = try? inlineAt(offset, tree: tree, model: model, text: text), inline.block.range.lowerBound == offset { 259 } else if let inline = try? inlineAt(offset, tree: tree, model: model, text: text), inline.block.range.lowerBound == offset {
251 return value(inline.params, language: inline.block.language) 260 return value(inline.params, language: inline.block.language)
252 } else { 261 } else {
@@ -924,22 +933,7 @@ public enum Babel {
924 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [], 933 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [],
925 inline: Bool = false) throws { 934 inline: Bool = false) throws {
926 var layers: [[(String, String)]] = [defaults] 935 var layers: [[(String, String)]] = [defaults]
927 let language = block.language ?? "" 936 layers += Params.propertyLayers(language: block.language ?? "", heading: block.heading, model: model)
928 // `org-babel-params-from-properties`: the inherited `header-args`, then the inherited
929 // `header-args:LANG`, each the nearest value with `+` additions after it.
930 var levels = [model.fileProperties]
931 if let heading = block.heading {
932 levels += (model.ancestors(of: heading).reversed() + [heading]).map { model.headings[$0].properties }
933 }
934 for key in ["header-args", "header-args:\(language)"] {
935 var value: String?
936 for entries in levels {
937 for entry in entries where entry.key.lowercased() == key {
938 value = entry.additive ? [value, entry.value].compactMap { $0 }.joined(separator: " ") : entry.value
939 }
940 }
941 if let value { layers.append(Params.parse(value)) }
942 }
943 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0))) 937 let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
944 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) { 938 if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
945 var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) 939 var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
@@ -993,6 +987,25 @@ public enum Babel {
993 } 987 }
994 } 988 }
995 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
996 /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and 1009 /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and
997 /// parentheses. 1010 /// parentheses.
998 /// A Lisp string's contents as `read` gives them. 1011 /// A Lisp string's contents as `read` gives them.
Tests/OrgCoreTests/BabelTests.swift +20
@@ -368,7 +368,27 @@ struct BabelCallTests {
368 (named + "Inline call_double[:results verbatim](n=1) here.\n", "call_"), 368 (named + "Inline call_double[:results verbatim](n=1) here.\n", "call_"),
369 (named + "Inline call_double(n=2) {{{results(=0=)}}} here.\n", "call_"), 369 (named + "Inline call_double(n=2) {{{results(=0=)}}} here.\n", "call_"),
370 (named + "Inline call_double(n=2) and call_double(n=3) here.\n", "call_double(n=3"), 370 (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_"),
371 ] 375 ]
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 """
372 392
373 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) 393 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
374 func matchesEmacs() throws { 394 func matchesEmacs() throws {
Tests/OrgCoreTests/ExportTests.swift +13
@@ -320,6 +320,19 @@ struct ExportResultsTests {
320 Inline src_sh[:exports both]{echo codeinlineboth} {{{results(=resinlineboth=)}}} three. 320 Inline src_sh[:exports both]{echo codeinlineboth} {{{results(=resinlineboth=)}}} three.
321 321
322 Inline call_named() {{{results(=resinlinecall=)}}} four. 322 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.
323 """ + "\n" 336 """ + "\n"
324 337
325 /// Which code and results the HTML and Markdown exports keep, against ox-html with nothing 338 /// Which code and results the HTML and Markdown exports keep, against ox-html with nothing