Commit 9ec764defc
Verified · cmc
Layout: unified · split
Sources/OrgCore/Export/ExportOptions.swift +28 −2
| @@ -194,6 +194,28 @@ public enum ExportPreprocess { | |||
| 194 | } | 194 | } |
| 195 | } | 195 | } |
| 196 | var counters: [String: Int] = [:] | 196 | var counters: [String: Int] = [:] |
| 197 | var model: DocumentModel? | ||
| 198 | /// `org-entry-get` with `selective` inheritance at the heading holding `location`, or | ||
| 199 | /// at the entry `search` finds (`*Title`, `#custom-id` or a title). | ||
| 200 | func property(_ name: String, _ search: String?, at location: Int) -> String { | ||
| 201 | if model == nil { model = DocumentModel(tree: OrgParser.parse(text)) } | ||
| 202 | let headings = model!.headings | ||
| 203 | let index: Int? | ||
| 204 | if let search, !search.isEmpty { | ||
| 205 | if search.hasPrefix("#") { | ||
| 206 | let id = String(search.dropFirst()) | ||
| 207 | index = headings.firstIndex { $0.properties.contains { $0.key.uppercased() == "CUSTOM_ID" && $0.value == id } } | ||
| 208 | } else { | ||
| 209 | let title = search.hasPrefix("*") ? String(search.dropFirst()) : search | ||
| 210 | index = headings.firstIndex { Links.displayFormat($0.title).trimmingCharacters(in: .whitespaces) == title || $0.title == title } | ||
| 211 | } | ||
| 212 | } else { | ||
| 213 | index = headings.lastIndex { $0.headingRange.lowerBound <= location } | ||
| 214 | } | ||
| 215 | guard let index else { return "" } | ||
| 216 | if name.uppercased() == "ITEM" { return headings[index].title } | ||
| 217 | return model!.property(name, of: index)?.value ?? "" | ||
| 218 | } | ||
| 197 | let regex = try! NSRegularExpression(pattern: "\\{\\{\\{([-A-Za-z0-9_]+)(?:\\((.*?)\\))?\\}\\}\\}") | 219 | let regex = try! NSRegularExpression(pattern: "\\{\\{\\{([-A-Za-z0-9_]+)(?:\\((.*?)\\))?\\}\\}\\}") |
| 198 | var out = "" | 220 | var out = "" |
| 199 | var at = 0 | 221 | var at = 0 |
| @@ -214,7 +236,10 @@ public enum ExportPreprocess { | |||
| 214 | continue | 236 | continue |
| 215 | } | 237 | } |
| 216 | let args = m.range(at: 2).location == NSNotFound ? [] : arguments(ns.substring(with: m.range(at: 2))) | 238 | let args = m.range(at: 2).location == NSNotFound ? [] : arguments(ns.substring(with: m.range(at: 2))) |
| 217 | out += expand(name, args, definitions: definitions, keywords: keywords, counters: &counters, path: path, now: now) | 239 | let location = m.range.location |
| 240 | out += expand(name, args, definitions: definitions, keywords: keywords, counters: &counters, path: path, now: now) { | ||
| 241 | property($0, $1, at: location) | ||
| 242 | } | ||
| 218 | } | 243 | } |
| 219 | out += ns.substring(from: at) | 244 | out += ns.substring(from: at) |
| 220 | return out | 245 | return out |
| @@ -245,7 +270,7 @@ public enum ExportPreprocess { | |||
| 245 | } | 270 | } |
| 246 | 271 | ||
| 247 | static func expand(_ name: String, _ args: [String], definitions: [String: String], keywords: [String: String], | 272 | static func expand(_ name: String, _ args: [String], definitions: [String: String], keywords: [String: String], |
| 248 | counters: inout [String: Int], path: String?, now: Date) -> String { | 273 | counters: inout [String: Int], path: String?, now: Date, property: (String, String?) -> String) -> String { |
| 249 | if let template = definitions[name] { | 274 | if let template = definitions[name] { |
| 250 | guard !template.hasPrefix("(eval") else { return "" } | 275 | guard !template.hasPrefix("(eval") else { return "" } |
| 251 | var result = template | 276 | var result = template |
| @@ -256,6 +281,7 @@ public enum ExportPreprocess { | |||
| 256 | case "title", "author", "date", "email": return keywords[name.uppercased()] ?? "" | 281 | case "title", "author", "date", "email": return keywords[name.uppercased()] ?? "" |
| 257 | case "keyword": return args.first.flatMap { keywords[$0.uppercased()] } ?? "" | 282 | case "keyword": return args.first.flatMap { keywords[$0.uppercased()] } ?? "" |
| 258 | case "input-file": return path.map { ($0 as NSString).lastPathComponent } ?? "" | 283 | case "input-file": return path.map { ($0 as NSString).lastPathComponent } ?? "" |
| 284 | case "property": return args.first.map { property($0, args.count > 1 ? args[1] : nil) } ?? "" | ||
| 259 | case "n": | 285 | case "n": |
| 260 | let counter = args.first ?? "" | 286 | let counter = args.first ?? "" |
| 261 | let action = args.count > 1 ? args[1] : "" | 287 | let action = args.count > 1 ? args[1] : "" |
Tests/OrgCoreTests/ExportOptionsTests.swift +28
| @@ -44,6 +44,34 @@ struct ExportOptionsTests { | |||
| 44 | #expect(!selected.contains("<p>a</p>") && selected.contains("<p>b</p>") && selected.contains("<p>c</p>")) | 44 | #expect(!selected.contains("<p>a</p>") && selected.contains("<p>b</p>") && selected.contains("<p>c</p>")) |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | @Test(.enabled(if: oracle)) | ||
| 48 | func expandsPropertyMacroLikeOrg() throws { | ||
| 49 | let text = """ | ||
| 50 | #+PROPERTY: color filecolor | ||
| 51 | Top: [{{{property(color)}}}] | ||
| 52 | * One | ||
| 53 | :PROPERTIES: | ||
| 54 | :color: red | ||
| 55 | :Size: big | ||
| 56 | :CUSTOM_ID: one | ||
| 57 | :END: | ||
| 58 | In one: [{{{property(color)}}}] [{{{property(SIZE)}}}] [{{{property(missing)}}}] [{{{property(ITEM)}}}] | ||
| 59 | ** Child | ||
| 60 | Child: [{{{property(color)}}}] | ||
| 61 | * Two | ||
| 62 | Two: [{{{property(color,*One)}}}] [{{{property(color,#one)}}}] [{{{property(Size, One)}}}] | ||
| 63 | """ + "\n" | ||
| 64 | let emacs = try EmacsOracle.evaluate(text, """ | ||
| 65 | (progn | ||
| 66 | (require 'ox) | ||
| 67 | (org-macro-initialize-templates) | ||
| 68 | (org-macro-replace-all org-macro-templates) | ||
| 69 | (list (buffer-substring-no-properties (point-min) (point-max)))) | ||
| 70 | """) | ||
| 71 | let ours = ExportPreprocess.macros(text, path: nil) | ||
| 72 | #expect(ours.contains("[red] [big] [] [One]") && ours == emacs.first, "ours:\n\(ours)\nemacs:\n\(emacs.first ?? "")") | ||
| 73 | } | ||
| 74 | |||
| 47 | @Test(.enabled(if: oracle)) | 75 | @Test(.enabled(if: oracle)) |
| 48 | func expandsIncludesAndMacrosLikeOrg() throws { | 76 | func expandsIncludesAndMacrosLikeOrg() throws { |
| 49 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-include-\(UUID().uuidString)") | 77 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-include-\(UUID().uuidString)") |