krz/orgstar

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

Commit ebd884b5da

ebd884b5dad3fccbaba63cd292e9fa28a996b6d6

parent: 78d3547c3b

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:51 UTC

Export :exports as Babel resolves header arguments

HTML and Markdown export read :exports with Babel.Params, so header-args
properties, #+HEADER: lines and the language defaults (dot, plantuml,
latex and others: results) count, not only the #+begin_src line.
#+CALL: lines and inline calls export their results, as
org-babel-default-lob-header-args says, and never code. Inline src
blocks default to :exports results; {{{results(...)}}} after an inline
block or call shows as :exports allows, and elsewhere as its argument.
Calls get the lob defaults over the called block's header arguments
when evaluated too, and inline defaults sit under the properties.

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +44 −7
@@ -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: [[(String, String)]] = [] 193 var extra = [lobDefaults]
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,49 @@ 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: [[(String, String)]] = [] 219 var extra = [lobDefaults]
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 /// `org-babel-default-lob-header-args`, over the called block's own header arguments.
227 static let lobDefaults = [(":exports", "results")]
228
229 /// `:exports` from the language's `org-babel-default-header-args:LANG` in Org 9.8.7.
230 static let languageExports = ["dot": "results", "plantuml": "results", "ditaa": "results", "gnuplot": "results",
231 "latex": "results", "lilypond": "results"]
232
233 /// What export shows of the src block, `#+CALL:` line, inline call or inline src block
234 /// starting at `offset` (`org-babel-exp-process-buffer`): `code`, `results`, `both` or
235 /// `none`, from the header arguments as evaluation resolves them. Calls never show code.
236 /// Nil when nothing there runs.
237 public static func exports(at offset: Int, tree: OrgTree, model: DocumentModel, text: NSString) -> String? {
238 func value(_ params: Params?, language: String?) -> String {
239 (params?.single[":exports"] ?? language.flatMap { languageExports[$0] } ?? "code").lowercased()
240 }
241 if let block = model.srcBlocks.first(where: { $0.range.lowerBound == offset }) {
242 return value(try? Params(block: block, model: model, text: text), language: block.language)
243 }
244 let line = text.substring(with: text.lineRange(for: NSRange(location: min(offset, text.length), length: 0)))
245 let call: Params?
246 if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil {
247 call = (try? callAt(offset, model: model, text: text))?.params
248 } 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
250 } 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)
252 } else {
253 return nil
254 }
255 // `org-babel-exp-code` for a call is `org-babel-exp-call-line-template`, empty.
256 switch call?.single[":exports"]?.lowercased() ?? "results" {
257 case "both", "results": return "results"
258 default: return "none"
259 }
260 }
261
226 /// Call arguments split on commas outside quotes and parentheses. 262 /// Call arguments split on commas outside quotes and parentheses.
227 static func splitArguments(_ s: String) -> [String] { 263 static func splitArguments(_ s: String) -> [String] {
228 var parts: [String] = [] 264 var parts: [String] = []
@@ -255,9 +291,8 @@ public enum Babel {
255 guard let object = found, let m = object.text.firstMatch(of: /^src_([^\s\[{]+)(?:\[([^\]]*)\])?\{(.*)\}$/) else { return nil } 291 guard let object = found, let m = object.text.firstMatch(of: /^src_([^\s\[{]+)(?:\[([^\]]*)\])?\{(.*)\}$/) else { return nil }
256 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= object.range.lowerBound } 292 let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= object.range.lowerBound }
257 let block = SrcBlockInfo(range: object.range, heading: heading, name: nil, language: String(m.1), switches: [], body: String(m.3), headerArguments: [:]) 293 let block = SrcBlockInfo(range: object.range, heading: heading, name: nil, language: String(m.1), switches: [], body: String(m.3), headerArguments: [:])
258 var extra: [[(String, String)]] = [[(":exports", "results"), (":hlines", "yes")]] 294 let extra = m.2.map { [Params.parse(String($0))] } ?? []
259 if let headers = m.2 { extra.append(Params.parse(String(headers))) } 295 return (block, try Params(block: block, model: model, text: text, defaults: [(":exports", "results"), (":hlines", "yes")], extra: extra, inline: true))
260 return (block, try Params(block: block, model: model, text: text, extra: extra, inline: true))
261 } 296 }
262 297
263 /// What `:var` references resolve to while planning. 298 /// What `:var` references resolve to while planning.
@@ -882,8 +917,10 @@ public enum Babel {
882 ["output", "value"], 917 ["output", "value"],
883 ] 918 ]
884 919
885 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, extra: [[(String, String)]] = [], inline: Bool = false) throws { 920 /// `defaults` go under the properties, as `org-babel-default-inline-header-args` do.
886 var layers: [[(String, String)]] = [] 921 init(block: SrcBlockInfo, model: DocumentModel, text: NSString, defaults: [(String, String)] = [], extra: [[(String, String)]] = [],
922 inline: Bool = false) throws {
923 var layers: [[(String, String)]] = [defaults]
887 let language = block.language ?? "" 924 let language = block.language ?? ""
888 // `org-babel-params-from-properties`: the inherited `header-args`, then the inherited 925 // `org-babel-params-from-properties`: the inherited `header-args`, then the inherited
889 // `header-args:LANG`, each the nearest value with `+` additions after it. 926 // `header-args:LANG`, each the nearest value with `+` additions after it.
Sources/OrgCore/Export/ExportOptions.swift +5
@@ -208,6 +208,11 @@ public enum ExportPreprocess {
208 continue 208 continue
209 } 209 }
210 let name = ns.substring(with: m.range(at: 1)).lowercased() 210 let name = ns.substring(with: m.range(at: 1)).lowercased()
211 // Inline results stay for the exporter, which shows them as `:exports` says.
212 if name == "results", definitions[name] == nil {
213 out += ns.substring(with: m.range)
214 continue
215 }
211 let args = m.range(at: 2).location == NSNotFound ? [] : arguments(ns.substring(with: m.range(at: 2))) 216 let args = m.range(at: 2).location == NSNotFound ? [] : arguments(ns.substring(with: m.range(at: 2)))
212 out += expand(name, args, definitions: definitions, keywords: keywords, counters: &counters, path: path, now: now) 217 out += expand(name, args, definitions: definitions, keywords: keywords, counters: &counters, path: path, now: now)
213 } 218 }
Sources/OrgCore/Export/HTMLExport.swift +57 −16
@@ -93,6 +93,33 @@ public enum HTMLExport {
93 } 93 }
94} 94}
95 95
96/// The tree being exported, for what its src blocks, calls and inline blocks export.
97final class ExportSource {
98 let tree: OrgTree
99 let text: NSString
100 private lazy var model = DocumentModel(tree: tree)
101 /// Whether the last inline src block or call lets the `{{{results(…)}}}` after it show;
102 /// nil when nothing asks.
103 var inlineResults: Bool?
104
105 init(tree: OrgTree, text: String) {
106 self.tree = tree
107 self.text = text as NSString
108 }
109
110 func exports(at offset: Int) -> String? {
111 Babel.exports(at: offset, tree: tree, model: model, text: text)
112 }
113
114 /// `{{{results(…)}}}`, Org's `$1` macro, as the inline block or call before it allows:
115 /// nil for other macros, the text to show (or nil) for this one.
116 func resultsMacro(_ node: SyntaxNode) -> String?? {
117 guard let m = node.text.firstMatch(of: /(?i)^\{\{\{results\((.*)\)\}\}\}$/) else { return .none }
118 defer { inlineResults = nil }
119 return inlineResults ?? true ? ExportPreprocess.arguments(String(m.1)).first : .some(nil)
120 }
121}
122
96final class HTMLRenderer { 123final class HTMLRenderer {
97 let settings: OrgSettings 124 let settings: OrgSettings
98 let options: ExportOptions 125 let options: ExportOptions
@@ -144,11 +171,23 @@ final class HTMLRenderer {
144 collectDefinitions(tree.root) 171 collectDefinitions(tree.root)
145 selected = Self.selection(tree, tags: options.selectTags) 172 selected = Self.selection(tree, tags: options.selectTags)
146 var out = "" 173 var out = ""
147 for child in tree.root.children { out += element(child) } 174 rendering(tree, text) {
175 for child in tree.root.children { out += element(child) }
176 }
148 out += footnoteSection() 177 out += footnoteSection()
149 return out 178 return out
150 } 179 }
151 180
181 private var source: ExportSource?
182
183 /// `body` with `tree` as the source of `:exports`.
184 private func rendering<T>(_ tree: OrgTree, _ text: String, _ body: () -> T) -> T {
185 let saved = source
186 source = ExportSource(tree: tree, text: text)
187 defer { source = saved }
188 return body()
189 }
190
152 private func collectDefinitions(_ node: SyntaxNode) { 191 private func collectDefinitions(_ node: SyntaxNode) {
153 for child in node.children { 192 for child in node.children {
154 if child.kind == .footnoteDefinition { 193 if child.kind == .footnoteDefinition {
@@ -181,7 +220,9 @@ final class HTMLRenderer {
181 func blocks(_ text: String) -> String { 220 func blocks(_ text: String) -> String {
182 let tree = OrgParser.parse(text, defaults: settings) 221 let tree = OrgParser.parse(text, defaults: settings)
183 var out = "" 222 var out = ""
184 for child in tree.root.children { out += element(child) } 223 rendering(tree, text) {
224 for child in tree.root.children { out += element(child) }
225 }
185 return out 226 return out
186 } 227 }
187 228
@@ -189,7 +230,7 @@ final class HTMLRenderer {
189 func inlineString(_ text: String) -> String { 230 func inlineString(_ text: String) -> String {
190 let tree = OrgParser.parse(text, defaults: settings) 231 let tree = OrgParser.parse(text, defaults: settings)
191 guard let paragraph = tree.root.descendants().first(where: { $0.kind == .paragraph }) else { return Self.escape(text) } 232 guard let paragraph = tree.root.descendants().first(where: { $0.kind == .paragraph }) else { return Self.escape(text) }
192 return inline(paragraph).trimmingCharacters(in: .whitespacesAndNewlines) 233 return rendering(tree, text) { inline(paragraph).trimmingCharacters(in: .whitespacesAndNewlines) }
193 } 234 }
194 235
195 /// A citation's or a bibliography entry's pieces: text escaped, prefixes and suffixes as 236 /// A citation's or a bibliography entry's pieces: text escaped, prefixes and suffixes as
@@ -209,8 +250,8 @@ final class HTMLRenderer {
209 // MARK: - Elements 250 // MARK: - Elements
210 251
211 private var affiliated: [String: String] = [:] 252 private var affiliated: [String: String] = [:]
212 /// `:exports` of the last src block: its results are exported only for `results` and 253 /// `:exports` of the last src block or call: its results are exported only for `results`
213 /// `both`; nothing runs. 254 /// and `both`; nothing runs.
214 private var lastExports = "code" 255 private var lastExports = "code"
215 256
216 func element(_ node: SyntaxNode) -> String { 257 func element(_ node: SyntaxNode) -> String {
@@ -219,7 +260,7 @@ final class HTMLRenderer {
219 defer { lastExports = "code" } 260 defer { lastExports = "code" }
220 if lastExports != "results", lastExports != "both" { return "" } 261 if lastExports != "results", lastExports != "both" { return "" }
221 } 262 }
222 if node.kind == .block, let exports = Self.srcExports(node) { 263 if node.kind == .block || node.kind == .keyword, let exports = source?.exports(at: node.range.lowerBound) {
223 lastExports = exports 264 lastExports = exports
224 if exports == "results" || exports == "none" { return "" } 265 if exports == "results" || exports == "none" { return "" }
225 } 266 }
@@ -311,14 +352,6 @@ final class HTMLRenderer {
311 } 352 }
312 } 353 }
313 354
314 /// The `:exports` value of a src block, or nil for other blocks.
315 static func srcExports(_ node: SyntaxNode) -> String? {
316 guard DocumentModel.isSrcBlock(node) else { return nil }
317 let first = node.text.components(separatedBy: "\n").first ?? ""
318 if let m = first.firstMatch(of: /:exports[ \t]+(\S+)/) { return m.1.lowercased() }
319 return "code"
320 }
321
322 private func slug(_ text: String) -> String { 355 private func slug(_ text: String) -> String {
323 var slug = "" 356 var slug = ""
324 var dash = false 357 var dash = false
@@ -706,8 +739,14 @@ final class HTMLRenderer {
706 let inner = braced ? String(body.dropFirst().dropLast()) : String(body) 739 let inner = braced ? String(body.dropFirst().dropLast()) : String(body)
707 return text.hasPrefix("_") ? "<sub>\(Self.escape(inner))</sub>" : "<sup>\(Self.escape(inner))</sup>" 740 return text.hasPrefix("_") ? "<sub>\(Self.escape(inner))</sub>" : "<sup>\(Self.escape(inner))</sup>"
708 case .lineBreak: return "<br>\n" 741 case .lineBreak: return "<br>\n"
709 case .target, .macro: return "" 742 case .macro:
743 guard case .some(let value) = source?.resultsMacro(node) else { return "" }
744 return value.map(inlineString) ?? ""
745 case .target: return ""
710 case .inlineSourceBlock: 746 case .inlineSourceBlock:
747 let exports = source?.exports(at: node.range.lowerBound) ?? "results"
748 source?.inlineResults = exports == "results" || exports == "both"
749 guard exports == "code" || exports == "both" else { return "" }
711 if let m = node.text.firstMatch(of: /\{(.*)\}$/) { return "<code>\(Self.escape(String(m.1)))</code>" } 750 if let m = node.text.firstMatch(of: /\{(.*)\}$/) { return "<code>\(Self.escape(String(m.1)))</code>" }
712 return Self.escape(node.text) 751 return Self.escape(node.text)
713 case .latexFragment: 752 case .latexFragment:
@@ -724,7 +763,9 @@ final class HTMLRenderer {
724 if inTitle { return "" } 763 if inTitle { return "" }
725 guard let pieces = citations?.next(node.text) else { return Self.escape(node.text) } 764 guard let pieces = citations?.next(node.text) else { return Self.escape(node.text) }
726 return render(pieces) 765 return render(pieces)
727 case .inlineBabelCall: return Self.escape(node.text) 766 case .inlineBabelCall:
767 source?.inlineResults = (source?.exports(at: node.range.lowerBound) ?? "results") == "results"
768 return ""
728 default: return inline(node) 769 default: return inline(node)
729 } 770 }
730 } 771 }
Sources/OrgCore/Export/MarkdownExport.swift +28 −6
@@ -38,7 +38,7 @@ final class MarkdownRenderer {
38 definitions[String(m.1)] = String(node.text[m.range.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) 38 definitions[String(m.1)] = String(node.text[m.range.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
39 } 39 }
40 } 40 }
41 var out = blocks(tree.root) 41 var out = rendering(tree, text) { blocks(tree.root) }
42 if !notes.isEmpty { 42 if !notes.isEmpty {
43 out += notes.map { "[^\($0.0)]: " + $0.1 }.joined(separator: "\n") + "\n" 43 out += notes.map { "[^\($0.0)]: " + $0.1 }.joined(separator: "\n") + "\n"
44 } 44 }
@@ -48,7 +48,17 @@ final class MarkdownRenderer {
48 func inlineString(_ text: String) -> String { 48 func inlineString(_ text: String) -> String {
49 let tree = OrgParser.parse(text, defaults: settings) 49 let tree = OrgParser.parse(text, defaults: settings)
50 guard let paragraph = tree.root.descendants().first(where: { $0.kind == .paragraph }) else { return text } 50 guard let paragraph = tree.root.descendants().first(where: { $0.kind == .paragraph }) else { return text }
51 return inline(paragraph).trimmingCharacters(in: .whitespacesAndNewlines) 51 return rendering(tree, text) { inline(paragraph).trimmingCharacters(in: .whitespacesAndNewlines) }
52 }
53
54 private var source: ExportSource?
55
56 /// `body` with `tree` as the source of `:exports`.
57 private func rendering<T>(_ tree: OrgTree, _ text: String, _ body: () -> T) -> T {
58 let saved = source
59 source = ExportSource(tree: tree, text: text)
60 defer { source = saved }
61 return body()
52 } 62 }
53 63
54 private var lastExports = "code" 64 private var lastExports = "code"
@@ -66,7 +76,7 @@ final class MarkdownRenderer {
66 defer { lastExports = "code" } 76 defer { lastExports = "code" }
67 if lastExports != "results", lastExports != "both" { return "" } 77 if lastExports != "results", lastExports != "both" { return "" }
68 } 78 }
69 if child.kind == .block, let exports = HTMLRenderer.srcExports(child) { 79 if child.kind == .block || child.kind == .keyword, let exports = source?.exports(at: child.range.lowerBound) {
70 lastExports = exports 80 lastExports = exports
71 if exports == "results" || exports == "none" { return "" } 81 if exports == "results" || exports == "none" { return "" }
72 } 82 }
@@ -76,7 +86,8 @@ final class MarkdownRenderer {
76 } 86 }
77 87
78 private func blocks(text: String) -> String { 88 private func blocks(text: String) -> String {
79 blocks(OrgParser.parse(text, defaults: settings).root) 89 let tree = OrgParser.parse(text, defaults: settings)
90 return rendering(tree, text) { blocks(tree.root) }
80 } 91 }
81 92
82 private func element(_ node: SyntaxNode, indent: String) -> String { 93 private func element(_ node: SyntaxNode, indent: String) -> String {
@@ -274,7 +285,16 @@ final class MarkdownRenderer {
274 let inner = body.hasPrefix("{") && body.hasSuffix("}") ? String(body.dropFirst().dropLast()) : String(body) 285 let inner = body.hasPrefix("{") && body.hasSuffix("}") ? String(body.dropFirst().dropLast()) : String(body)
275 return node.text.hasPrefix("_") ? "<sub>\(inner)</sub>" : "<sup>\(inner)</sup>" 286 return node.text.hasPrefix("_") ? "<sub>\(inner)</sub>" : "<sup>\(inner)</sup>"
276 case .lineBreak: return " \n" 287 case .lineBreak: return " \n"
277 case .target, .macro: return "" 288 case .macro:
289 guard case .some(let value) = source?.resultsMacro(node) else { return "" }
290 return value.map(inlineString) ?? ""
291 case .target: return ""
292 case .inlineSourceBlock:
293 let exports = source?.exports(at: node.range.lowerBound) ?? "results"
294 source?.inlineResults = exports == "results" || exports == "both"
295 guard exports == "code" || exports == "both" else { return "" }
296 if let m = node.text.firstMatch(of: /\{(.*)\}$/) { return code(String(m.1)) }
297 return node.text
278 // ox-md uses ox-html's translator: only HTML snippets. 298 // ox-md uses ox-html's translator: only HTML snippets.
279 case .exportSnippet: 299 case .exportSnippet:
280 guard node.tokens.first?.text == "@@html:" else { return "" } 300 guard node.tokens.first?.text == "@@html:" else { return "" }
@@ -283,7 +303,9 @@ final class MarkdownRenderer {
283 if inTitle { return "" } 303 if inTitle { return "" }
284 guard let pieces = citations?.next(node.text) else { return node.text } 304 guard let pieces = citations?.next(node.text) else { return node.text }
285 return render(pieces) 305 return render(pieces)
286 case .inlineBabelCall: return node.text 306 case .inlineBabelCall:
307 source?.inlineResults = (source?.exports(at: node.range.lowerBound) ?? "results") == "results"
308 return ""
287 default: return inline(node) 309 default: return inline(node)
288 } 310 }
289 } 311 }
Tests/OrgCoreTests/ExportTests.swift +81
@@ -220,4 +220,85 @@ struct ExportResultsTests {
220 #expect(html.contains("echo b") && html.contains(">b</pre>")) 220 #expect(html.contains("echo b") && html.contains(">b</pre>"))
221 #expect(!html.contains("echo c") && html.contains(">c</pre>")) 221 #expect(!html.contains("echo c") && html.contains(">c</pre>"))
222 } 222 }
223
224 static let exports = """
225 #+PROPERTY: header-args :exports both
226 * Heading
227 :PROPERTIES:
228 :header-args:sh: :exports results
229 :END:
230 #+begin_src sh
231 echo codeprop
232 #+end_src
233
234 #+RESULTS:
235 : resprop
236
237 #+HEADER: :exports code
238 #+begin_src sh
239 echo codeheader
240 #+end_src
241
242 #+RESULTS:
243 : resheader
244
245 #+begin_src python
246 codefile
247 #+end_src
248
249 #+RESULTS:
250 : resfile
251
252 #+NAME: named
253 #+begin_src sh :exports none
254 echo codenamed
255 #+end_src
256
257 #+CALL: named()
258
259 #+RESULTS:
260 : rescall
261
262 #+CALL: named() :exports code
263
264 #+RESULTS:
265 : rescallcode
266
267 #+begin_src dot :file graph.png
268 digraph { codedot }
269 #+end_src
270
271 #+RESULTS:
272 : resdot
273
274 Inline src_sh{echo codeinline} {{{results(=resinline=)}}} one.
275
276 Inline src_sh[:exports code]{echo codeinlinecode} {{{results(=resinlinecode=)}}} two.
277
278 Inline src_sh[:exports both]{echo codeinlineboth} {{{results(=resinlineboth=)}}} three.
279
280 Inline call_named() {{{results(=resinlinecall=)}}} four.
281 """ + "\n"
282
283 /// Which code and results the HTML and Markdown exports keep, against ox-html with nothing
284 /// evaluated.
285 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
286 func exportsLikeOrg() throws {
287 let emacs = try EmacsOracle.evaluate(Self.exports, """
288 (progn
289 (require 'ox-html) (require 'ob-shell) (require 'ob-python) (require 'ob-dot)
290 (dolist (v '(org-babel-default-header-args org-babel-default-inline-header-args org-babel-default-lob-header-args))
291 (set v (cons '(:eval . "never-export") (symbol-value v))))
292 (list (org-export-as 'html nil nil t)))
293 """)[0]
294 let words = Self.exports.matches(of: /\b(?:code|res)[a-z]+\b/).map { String($0.0) }
295 let expected = Set(words.filter { emacs.contains($0) })
296 #expect(expected.count > 4 && expected.count < words.count)
297 let html = HTMLExport.page(Self.exports, fallbackTitle: "t")
298 let markdown = MarkdownExport.document(Self.exports)
299 for word in words {
300 #expect(html.contains(word) == expected.contains(word), "HTML \(word)")
301 #expect(markdown.contains(word) == expected.contains(word), "Markdown \(word)")
302 }
303 }
223} 304}