Sources/OrgSwift/AST/OrgFootnotes.swift
57 lines · 2327 bytes
1import Foundation
2
3/// Assigns footnote numbers in first-reference order and holds the notes they resolve to.
4///
5/// Numbering is document-wide and order-of-use, so it belongs to the render pass rather than
6/// the parse: a renderer walks the tree, calls ``number(for:inline:)`` as it meets each
7/// reference, then reads ``notes`` to emit the section. It is output-format agnostic —
8/// the HTML renderer and the SwiftUI one share it.
9package struct OrgFootnotes {
10 private var numbers: [String: Int] = [:]
11 private var order: [String] = []
12 /// Reference-style definitions, gathered from the document's `[fn:x] …` lines.
13 private var definitions: [String: [OrgObject]] = [:]
14 /// Inline definitions, gathered from `[fn:x:text]` references as they are rendered.
15 private var inlineDefinitions: [String: [OrgObject]] = [:]
16 package var figureNumber = 0
17
18 package init(document: OrgDocument) {
19 for element in document.elements {
20 if case .footnoteDefinition(let label, let content) = element {
21 definitions[label] = content
22 }
23 }
24 }
25
26 package mutating func number(for label: String, inline: [OrgObject]? = nil) -> Int {
27 if let inline, inlineDefinitions[label] == nil { inlineDefinitions[label] = inline }
28 if let existing = numbers[label] { return existing }
29 let next = order.count + 1
30 numbers[label] = next
31 order.append(label)
32 return next
33 }
34
35 /// One numbered note, resolved to its content.
36 package struct Note {
37 package var number: Int
38 package var label: String
39 package var content: [OrgObject]
40 /// An inline footnote's text sits directly in the item; a reference-style definition
41 /// is a paragraph, matching org's exporter.
42 package var isInline: Bool
43 }
44
45 /// The referenced notes, in numbering order. Empty when the document has none, which is
46 /// the renderer's cue to omit the section entirely.
47 package var notes: [Note] {
48 order.map { label in
49 Note(
50 number: numbers[label] ?? 0,
51 label: label,
52 content: inlineDefinitions[label] ?? definitions[label] ?? [],
53 isInline: inlineDefinitions[label] != nil
54 )
55 }
56 }
57}