Commit ebd884b5da
Verified · cmc
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. | ||
| 97 | final 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 | |||
| 96 | final class HTMLRenderer { | 123 | final 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 | } |