Commit a4008c4a7f
Verified · cmc
Layout: unified · split
Sources/OrgCore/Export/Citations.swift +27 −5
| @@ -3,8 +3,8 @@ import Foundation | |||
| 3 | /// Citations as the `basic` processor (oc-basic.el, Org 9.8.7) exports them: author-year, | 3 | /// Citations as the `basic` processor (oc-basic.el, Org 9.8.7) exports them: author-year, |
| 4 | /// author, noauthor, text, nocite and numeric styles with the bare and caps variants, from | 4 | /// author, noauthor, text, nocite and numeric styles with the bare and caps variants, from |
| 5 | /// the BibTeX or CSL-JSON files `#+bibliography:` names, and the bibliography | 5 | /// the BibTeX or CSL-JSON files `#+bibliography:` names, and the bibliography |
| 6 | /// `#+print_bibliography:` asks for. Note styles, and `#+cite_export:` processors other than | 6 | /// `#+print_bibliography:` asks for. Note styles become footnotes. `#+cite_export:` |
| 7 | /// `basic`, leave citations as they are written. | 7 | /// processors other than `basic` leave citations as they are written. |
| 8 | struct CiteProcessor { | 8 | struct CiteProcessor { |
| 9 | /// What a citation or bibliography entry exports as: text, Org markup (prefixes and | 9 | /// What a citation or bibliography entry exports as: text, Org markup (prefixes and |
| 10 | /// suffixes) and an italic title. | 10 | /// suffixes) and an italic title. |
| @@ -310,9 +310,7 @@ struct CiteProcessor { | |||
| 310 | return authorYear({ (bare ? [] : [.text("(")]) + $0 + $1 + $2 + (bare ? [] : [.text(")")]) }) { r, _, y in org(r.prefix) + [.text(y)] + org(r.suffix) } | 310 | return authorYear({ (bare ? [] : [.text("(")]) + $0 + $1 + $2 + (bare ? [] : [.text(")")]) }) { r, _, y in org(r.prefix) + [.text(y)] + org(r.suffix) } |
| 311 | case "nocite", "n": | 311 | case "nocite", "n": |
| 312 | return [] | 312 | return [] |
| 313 | case "note", "ft": | 313 | case "text", "t", "note", "ft": |
| 314 | return nil | ||
| 315 | case "text", "t": | ||
| 316 | return authorYear({ $0 + $1 + $2 }) { r, a, y in | 314 | return authorYear({ $0 + $1 + $2 }) { r, a, y in |
| 317 | org(r.prefix) + [.text(capitalized(a) + (bare ? " " : " (") + y + (bare ? "" : ")"))] + org(r.suffix) | 315 | org(r.prefix) + [.text(capitalized(a) + (bare ? " " : " (") + y + (bare ? "" : ")"))] + org(r.suffix) |
| 318 | } | 316 | } |
| @@ -421,6 +419,30 @@ struct CiteProcessor { | |||
| 421 | }.compactMap { entry($0.element, style: style, keys: keys) } | 419 | }.compactMap { entry($0.element, style: style, keys: keys) } |
| 422 | } | 420 | } |
| 423 | 421 | ||
| 422 | /// Note citations (`note`, `ft`) as inline footnotes holding their text-style citation, as | ||
| 423 | /// `org-cite-wrap-citation` does, the blanks before them gone and punctuation right after | ||
| 424 | /// them moved in front (`org-cite-adjust-note`). | ||
| 425 | mutating func inliningNotes(_ text: String) -> String { | ||
| 426 | let defaultStyle = Self.exportSettings(text)?.citeStyle.map { String($0.split(separator: "/").first ?? "") } | ||
| 427 | let regex = try! NSRegularExpression(pattern: "[ \\t]*(\\[cite[/:][^\\]]*\\])([.,;:!?]*)") | ||
| 428 | var out = "" | ||
| 429 | var at = 0 | ||
| 430 | let ns = text as NSString | ||
| 431 | for m in regex.matches(in: text, range: NSRange(location: 0, length: ns.length)) { | ||
| 432 | let written = ns.substring(with: m.range(at: 1)) | ||
| 433 | guard let citation = Self.parse(written), ["note", "ft"].contains(citation.style ?? defaultStyle ?? ""), | ||
| 434 | let pieces = next(written) else { continue } | ||
| 435 | let note = pieces.map { piece in | ||
| 436 | switch piece { | ||
| 437 | case .text(let s), .org(let s), .italic(let s): s | ||
| 438 | } | ||
| 439 | }.joined() | ||
| 440 | out += ns.substring(with: NSRange(location: at, length: m.range.location - at)) + ns.substring(with: m.range(at: 2)) + "[fn:: " + note + "]" | ||
| 441 | at = NSMaxRange(m.range) | ||
| 442 | } | ||
| 443 | return out + ns.substring(from: at) | ||
| 444 | } | ||
| 445 | |||
| 424 | /// The export of the next citation written as `text`; nil leaves it as written. | 446 | /// The export of the next citation written as `text`; nil leaves it as written. |
| 425 | mutating func next(_ text: String) -> [Piece]? { | 447 | mutating func next(_ text: String) -> [Piece]? { |
| 426 | guard var queue = outputs[text], !queue.isEmpty else { return nil } | 448 | guard var queue = outputs[text], !queue.isEmpty else { return nil } |
Sources/OrgCore/Export/HTMLExport.swift +1 −1
| @@ -55,7 +55,7 @@ public enum HTMLExport { | |||
| 55 | var header = options.title ? "<h1 class=\"title\">\(titleHTML)</h1>\n" : "" | 55 | var header = options.title ? "<h1 class=\"title\">\(titleHTML)</h1>\n" : "" |
| 56 | let byline = [options.author ? meta.author : nil, options.date ? meta.date : nil].compactMap { $0 }.filter { !$0.isEmpty } | 56 | let byline = [options.author ? meta.author : nil, options.date ? meta.date : nil].compactMap { $0 }.filter { !$0.isEmpty } |
| 57 | if !byline.isEmpty { header += "<p class=\"byline\">\(byline.map(HTMLRenderer.escape).joined(separator: " · "))</p>\n" } | 57 | if !byline.isEmpty { header += "<p class=\"byline\">\(byline.map(HTMLRenderer.escape).joined(separator: " · "))</p>\n" } |
| 58 | let body = renderer.document(renderer.citations == nil ? text : CiteProcessor.removingNocite(text)) | 58 | let body = renderer.document(renderer.citations == nil ? text : renderer.citations!.inliningNotes(CiteProcessor.removingNocite(text))) |
| 59 | header += renderer.tableOfContents() | 59 | header += renderer.tableOfContents() |
| 60 | var head = options.htmlHead.map { $0 + "\n" }.joined() | 60 | var head = options.htmlHead.map { $0 + "\n" }.joined() |
| 61 | if renderer.hasMath { | 61 | if renderer.hasMath { |
Sources/OrgCore/Export/MarkdownExport.swift +1 −1
| @@ -11,7 +11,7 @@ public enum MarkdownExport { | |||
| 11 | if let title = HTMLExport.metadata(text, settings: settings).title { out = "# " + renderer.inlineString(title) + "\n\n" } | 11 | if let title = HTMLExport.metadata(text, settings: settings).title { out = "# " + renderer.inlineString(title) + "\n\n" } |
| 12 | renderer.inTitle = false | 12 | renderer.inTitle = false |
| 13 | renderer.citations = CiteProcessor(text: text, directory: path.map { URL(fileURLWithPath: $0).deletingLastPathComponent() }) | 13 | renderer.citations = CiteProcessor(text: text, directory: path.map { URL(fileURLWithPath: $0).deletingLastPathComponent() }) |
| 14 | out += renderer.document(renderer.citations == nil ? text : CiteProcessor.removingNocite(text)) | 14 | out += renderer.document(renderer.citations == nil ? text : renderer.citations!.inliningNotes(CiteProcessor.removingNocite(text))) |
| 15 | return out.trimmingCharacters(in: .newlines) + "\n" | 15 | return out.trimmingCharacters(in: .newlines) + "\n" |
| 16 | } | 16 | } |
| 17 | } | 17 | } |
Tests/OrgCoreTests/CitationTests.swift +15
| @@ -109,6 +109,21 @@ struct CitationTests { | |||
| 109 | #expect(page.contains("<p>See (Doe, Jane and Roe, Richard, 2020).</p>")) | 109 | #expect(page.contains("<p>See (Doe, Jane and Roe, Richard, 2020).</p>")) |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | /// As ox-md exports them: footnotes after the punctuation, holding text-style citations. | ||
| 113 | @Test func noteCitationsBecomeFootnotes() throws { | ||
| 114 | let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-cite-\(UUID().uuidString)") | ||
| 115 | try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) | ||
| 116 | defer { try? FileManager.default.removeItem(at: folder) } | ||
| 117 | try Self.bib.write(to: folder.appendingPathComponent("refs.bib"), atomically: true, encoding: .utf8) | ||
| 118 | let text = "#+bibliography: refs.bib\nFirst claim [cite/ft:@doe2020]. Second [cite/note:see @smith1999 p. 5], and third [cite/ft:@lee].\n\nEnd [cite/ft/c:@lee]\n" | ||
| 119 | let markdown = MarkdownExport.document(text, path: folder.appendingPathComponent("doc.org").path) | ||
| 120 | #expect(markdown.contains("First claim.[^1] Second,[^2] and third.[^3]")) | ||
| 121 | #expect(markdown.contains("End[^4]")) | ||
| 122 | #expect(markdown.contains("[^1]: Doe, Jane and Roe, Richard (2020)")) | ||
| 123 | #expect(markdown.contains("[^2]: see Smith, John (1999) p. 5")) | ||
| 124 | #expect(markdown.contains("[^4]: Lee, Kim And Park, Min And Choi, Su (2021)")) | ||
| 125 | } | ||
| 126 | |||
| 112 | @Test(.enabled(if: oracle)) | 127 | @Test(.enabled(if: oracle)) |
| 113 | func authorYearMatchesOcBasic() throws { | 128 | func authorYearMatchesOcBasic() throws { |
| 114 | try compare("#+bibliography: refs.bib\n#+bibliography: more.json\n" + Self.body) | 129 | try compare("#+bibliography: refs.bib\n#+bibliography: more.json\n" + Self.body) |