Sources/OrgCore/Commands/Footnotes.swift
521 lines · 24036 bytes
1import Foundation
2
3// Footnotes (`org-footnote.el`, Org 9.8.7) with the defaults: `org-footnote-section`
4// "Footnotes", `org-footnote-auto-label` t, `org-footnote-define-inline` and
5// `org-footnote-auto-adjust` nil.
6
7extension EmacsBuffer {
8 struct FootnoteReference: Equatable {
9 /// Nil for anonymous references.
10 var label: String?
11 var begin: Int
12 /// After the closing bracket.
13 var end: Int
14 /// An inline definition's text.
15 var contents: Range<Int>?
16 /// The label of the definition the reference sits in.
17 var definition: String?
18 }
19
20 struct FootnoteDefinition {
21 var label: String
22 var begin: Int
23 var contentsBegin: Int?
24 /// The line after its last non-blank line.
25 var end: Int
26 }
27
28 static let footnoteSection = "Footnotes"
29
30 private func footnoteTree() -> OrgTree { OrgParser.parse(string) }
31
32 /// Definitions in buffer order, ending as `org-element--footnote-separator` ends them.
33 func footnoteDefinitions(_ tree: OrgTree? = nil) -> [FootnoteDefinition] {
34 let tree = tree ?? footnoteTree()
35 return tree.root.descendants().filter { $0.kind == .footnoteDefinition }.compactMap { node in
36 saveExcursion {
37 point = node.range.lowerBound
38 guard let match = lookingAt("\\[fn:([-_\\w]+)\\]") else { return nil }
39 let label = substring(match[1]!.lowerBound, match[1]!.upperBound)
40 point = lineEnd(point)
41 let separator = searchForward("^(?:\\*+ |\\[fn:[-_\\w]+\\]|([ \\t]*\\n){2,})", caseInsensitive: false)
42 var raw = length
43 if let separator {
44 raw = separator[0]!.lowerBound
45 if separator[1] != nil {
46 point = raw
47 skipForward([32, 13, 9, 10])
48 raw = eobp ? length : lineStart(point)
49 }
50 }
51 point = match[0]!.upperBound
52 skipForward([32, 13, 9, 10])
53 let contentsBegin: Int? = point >= raw ? nil : lineStart(point) == node.range.lowerBound ? point : lineStart(point)
54 point = raw
55 skipBackward([32, 13, 9, 10])
56 forwardLine(1)
57 return FootnoteDefinition(label: label, begin: node.range.lowerBound, contentsBegin: contentsBegin, end: point)
58 }
59 }
60 }
61
62 /// References in buffer order.
63 func footnoteReferences(_ tree: OrgTree? = nil) -> [FootnoteReference] {
64 let tree = tree ?? footnoteTree()
65 let definitions = footnoteDefinitions(tree)
66 return tree.root.descendants().filter { $0.kind == .footnoteReference }.compactMap { node in
67 saveExcursion {
68 point = node.range.lowerBound
69 guard let match = lookingAt("\\[fn:([-_\\w]*)(:)?") else { return nil }
70 let label = match[1]!.isEmpty ? nil : substring(match[1]!.lowerBound, match[1]!.upperBound)
71 let contents = match[2] != nil ? match[0]!.upperBound..<(node.range.upperBound - 1) : nil
72 let holder = definitions.last { $0.begin <= node.range.lowerBound && node.range.lowerBound < $0.end }
73 return FootnoteReference(label: label, begin: node.range.lowerBound, end: node.range.upperBound, contents: contents, definition: holder?.label)
74 }
75 }
76 }
77
78 /// `org-footnote-at-reference-p`.
79 func footnoteReferenceAtPoint() -> FootnoteReference? {
80 footnoteReferences().first { $0.begin <= point && point < $0.end }
81 }
82
83 /// `org-footnote-at-definition-p`: anywhere in a definition.
84 func footnoteDefinitionAtPoint() -> FootnoteDefinition? {
85 footnoteDefinitions().first { $0.begin <= point && point < max($0.end, $0.begin + 1) }
86 }
87
88 /// `org-footnote-all-labels`.
89 func footnoteLabels() -> [String] {
90 let tree = footnoteTree()
91 var all: [String] = []
92 for label in footnoteReferences(tree).compactMap(\.label) + footnoteDefinitions(tree).map(\.label) where !all.contains(label) {
93 all.append(label)
94 }
95 return all
96 }
97
98 /// `org-footnote-unique-label`.
99 func footnoteUniqueLabel(_ current: [String]) -> String {
100 var count = 1
101 while current.contains(String(count)) { count += 1 }
102 return String(count)
103 }
104
105 /// Where `org-footnote-get-definition` finds LABEL: its definition, or an inline reference.
106 func footnoteDefinitionStart(_ label: String) -> Int? {
107 let tree = footnoteTree()
108 let definition = footnoteDefinitions(tree).first { $0.label == label }?.begin
109 let inline = footnoteReferences(tree).first { $0.label == label && $0.contents != nil && $0.begin > 0 }?.begin
110 return [definition, inline].compactMap { $0 }.min()
111 }
112
113 /// `org-footnote--allow-reference-p` for a labeled reference.
114 func footnoteReferenceAllowed() -> Bool {
115 if bolp { return false }
116 let tree = footnoteTree()
117 var chain: [SyntaxNode] = []
118 var node = tree.root
119 while let child = node.child(containing: point) {
120 chain.append(child)
121 node = child
122 }
123 if chain.contains(where: { $0.kind == .heading }), let heading = chain.first(where: { $0.kind == .heading }),
124 lineStart(point) == heading.range.lowerBound {
125 guard let title = heading.firstChild(.title) else { return false }
126 let tags = heading.tokens.first { $0.kind == .tags }
127 return point >= title.range.lowerBound && tags.map { point < $0.range.lowerBound } ?? true
128 }
129 let objects: Set<SyntaxKind> = [.bold, .italic, .underline, .strikeThrough, .verbatim, .code, .link, .linkDescription, .timestamp,
130 .footnoteReference, .statisticsCookie, .target, .macro, .inlineSourceBlock, .latexFragment, .lineBreak, .superscript, .subscript, .entity, .radioTarget,
131 .citation, .citationReference, .exportSnippet, .inlineBabelCall, .itemTag]
132 let containers: Set<SyntaxKind> = [.document, .zerothSection, .section, .plainList, .item, .drawer]
133 let element = chain.last { !objects.contains($0.kind) && !containers.contains($0.kind) }
134 switch element?.kind {
135 case nil, .paragraph, .tableRow, .tableCell:
136 break
137 case .footnoteDefinition:
138 let definition = footnoteDefinitions(tree).first { $0.begin == element!.range.lowerBound }
139 if let begin = definition?.contentsBegin, point >= begin { break }
140 return character(at: point).map { [32, 9, 10, 13].contains($0) } ?? false
141 case .block:
142 let block = element!
143 let opening = saveExcursion { () -> Bool in
144 point = block.range.lowerBound
145 return lookingAt("[ \\t]*#\\+begin_verse") != nil
146 }
147 guard opening, let wrapper = Wrappers.starting(at: block.range.lowerBound, in: tree) else { return false }
148 let contentsBegin = lineEnd(block.range.lowerBound) + 1
149 return point >= contentsBegin && point < wrapper.closeLine
150 default:
151 // Blank lines after an element.
152 let after = saveExcursion { () -> Int in
153 point = element!.range.upperBound
154 skipBackward([32, 13, 9, 10])
155 forwardLine(1)
156 return point
157 }
158 return point >= after
159 }
160 guard let object = chain.last(where: { objects.contains($0.kind) }) else { return true }
161 if point == object.range.lowerBound { return true }
162 if chain.contains(where: { $0.kind == .link }) { return false }
163 switch object.kind {
164 case .bold, .italic, .underline, .strikeThrough, .superscript, .subscript, .radioTarget:
165 return point > object.range.lowerBound && point <= object.range.upperBound - 1
166 default:
167 return false
168 }
169 }
170
171 /// `org-back-over-empty-lines`, returning the number of empty lines passed.
172 @discardableResult
173 func backOverEmptyLinesCount() -> Int {
174 let position = point
175 backOverEmptyLines()
176 return countLines(point, position)
177 }
178
179 /// `org-end-of-meta-data` with FULL t.
180 func endOfMetaDataFull() {
181 endOfMetaData()
182 guard !atHeading else { return }
183 let end = saveExcursion { () -> Int in outlineNextHeading(); return point }
184 while !eobp {
185 if lookingAt("[ \\t]*$|^[ \\t]*CLOCK:") != nil {
186 forwardLine(1)
187 } else if lookingAt("[ \\t]*:[-_\\w]+:[ \\t]*$") != nil {
188 guard searchForward("^[ \\t]*:END:[ \\t]*$", bound: end, caseInsensitive: true) != nil else { return }
189 forwardLine(1)
190 } else {
191 return
192 }
193 }
194 }
195
196 /// `org-footnote--clear-footnote-section`.
197 func clearFootnoteSection() {
198 point = 0
199 let pattern = "^\\*+ +" + NSRegularExpression.escapedPattern(for: Self.footnoteSection) + "[ \\t]*$"
200 while let match = searchForward(pattern, caseInsensitive: false) {
201 point = match[0]!.lowerBound
202 let end = endOfSubtree(toHeading: true)
203 deleteRegion(match[0]!.lowerBound, end)
204 point = match[0]!.lowerBound
205 }
206 point = length
207 skipBackward([32, 13, 9, 10])
208 if !bobp {
209 forwardLine(1)
210 if eolp { insert("\n") }
211 }
212 deleteRegion(point, length)
213 if saveExcursion({ backOverEmptyLinesCount() }) == 0 { insert("\n") }
214 insert("* " + Self.footnoteSection + "\n")
215 }
216
217 /// `org-footnote-create-definition`: the definition's start.
218 func createFootnoteDefinition(_ label: String) -> Int {
219 saveExcursion {
220 let pattern = "^\\*+[ \\t]+" + NSRegularExpression.escapedPattern(for: Self.footnoteSection) + "[ \\t]*$"
221 point = 0
222 if let match = searchForward(pattern, caseInsensitive: false) {
223 point = match[0]!.upperBound
224 endOfMetaDataFull()
225 if !bolp { insert("\n") }
226 } else {
227 clearFootnoteSection()
228 }
229 if backOverEmptyLinesCount() == 0 { insert("\n") }
230 insert("[fn:\(label)] \n")
231 forwardLine(-1)
232 return point
233 }
234 }
235
236 /// `org-footnote-goto-definition`.
237 func gotoFootnoteDefinition(_ label: String, at start: Int) {
238 point = start
239 if let match = lookingAt("\\[fn:" + NSRegularExpression.escapedPattern(for: label) + "[]:]") { point = match[0]!.upperBound }
240 }
241
242 /// `org-footnote-new`.
243 func newFootnote() throws {
244 let all = footnoteLabels()
245 let label = footnoteUniqueLabel(all)
246 guard footnoteReferenceAllowed() else { throw UserError(message: "Cannot insert a footnote here") }
247 insert("[fn:\(label)]")
248 gotoFootnoteDefinition(label, at: createFootnoteDefinition(label))
249 }
250
251 /// `org-footnote-goto-previous-reference`.
252 func gotoPreviousFootnoteReference(_ label: String) throws {
253 let references = footnoteReferences().filter { $0.label == label }
254 let opening = "[fn:\(label)".utf16.count + 1
255 guard let reference = references.last(where: { $0.begin + opening <= point }) ?? references.first(where: { $0.begin >= point }) else {
256 throw UserError(message: "Cannot find reference of footnote \"\(label)\"")
257 }
258 point = reference.begin
259 }
260
261 /// `org-footnote--set-label`, with point at the reference or definition.
262 func setFootnoteLabel(_ label: String) {
263 point += 4
264 if character(at: point) == 58 {
265 insert(label)
266 } else if let match = lookingAt("([-_\\w]+)") {
267 replace(match[1]!, with: label)
268 point = match[1]!.lowerBound + label.utf16.count
269 }
270 }
271
272 /// `org-footnote--collect-references`, in org's reading order: nested references follow
273 /// the definition they sit in.
274 func collectFootnoteReferences(anonymous: Bool = false) -> [FootnoteReference] {
275 let references = footnoteReferences().filter { anonymous || $0.label != nil }
276 var nested: [String: [String?]] = [:]
277 for reference in references { if let holder = reference.definition { nested[holder, default: []].append(reference.label) } }
278 var ordered: [FootnoteReference] = []
279 func add(_ reference: FootnoteReference, allowNested: Bool) {
280 guard allowNested || reference.definition == nil else { return }
281 ordered.append(reference)
282 for label in nested[reference.label ?? ""] ?? [] where reference.label != nil {
283 guard let r = references.last(where: { $0.label == label }), !ordered.contains(r) else { continue }
284 add(r, allowNested: true)
285 }
286 }
287 for reference in references { add(reference, allowNested: false) }
288 return ordered
289 }
290
291 /// `org-footnote--collect-definitions`: label and text, last first, as org's alist.
292 func collectFootnoteDefinitions(delete: Bool) -> [(label: String, text: String)] {
293 var seen: Set<String> = []
294 var found: [(label: String, text: String, range: Range<Int>)] = []
295 for definition in footnoteDefinitions() where !seen.contains(definition.label) {
296 seen.insert(definition.label)
297 let begin = saveExcursion { () -> Int in
298 point = definition.begin
299 skipBackward([32, 13, 9, 10])
300 if bobp { return point }
301 forwardLine(1)
302 return point
303 }
304 let text = substring(begin, definition.end).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r"))
305 found.append((definition.label, text, begin..<definition.end))
306 }
307 if delete { for f in found.reversed() { deleteRegion(f.range.lowerBound, f.range.upperBound) } }
308 return found.reversed().map { ($0.label, $0.text) }
309 }
310
311 /// `org-footnote-renumber-fn:N`.
312 func renumberFootnotes() {
313 var c = 0
314 let references = collectFootnoteReferences().filter { $0.label!.range(of: "^[0-9]+$", options: .regularExpression) != nil }
315 var alist: [String: String] = [:]
316 for label in references.map({ $0.label! }) where alist[label] == nil {
317 c += 1
318 alist[label] = String(c)
319 }
320 let markers = references.map { marker($0.begin) }
321 for (reference, m) in zip(references, markers) {
322 point = m.position
323 setFootnoteLabel(alist[reference.label!]!)
324 }
325 markers.forEach(release)
326 point = 0
327 while let match = searchForward("^\\[fn:([0-9]+)\\]", caseInsensitive: false) {
328 let old = substring(match[1]!.lowerBound, match[1]!.upperBound)
329 let new = alist[old] ?? { c += 1; return String(c) }()
330 replace(match[1]!, with: new)
331 point = match[1]!.lowerBound + new.utf16.count + 1
332 }
333 }
334
335 /// `org-footnote-sort`.
336 func sortFootnotes() {
337 let references = collectFootnoteReferences()
338 let definitions = collectFootnoteDefinitions(delete: true)
339 clearFootnoteSection()
340 var inserted: [String] = []
341 for reference in references where !inserted.contains(reference.label!) && reference.contents == nil {
342 inserted.append(reference.label!)
343 insert("\n" + (definitions.first { $0.label == reference.label }?.text ?? "[fn:\(reference.label!)] DEFINITION NOT FOUND.") + "\n")
344 }
345 for definition in definitions where !inserted.contains(definition.label) {
346 insert("\n" + definition.text + "\n")
347 }
348 }
349
350 /// `org-footnote-normalize`.
351 func normalizeFootnotes() {
352 enum Key: Equatable {
353 case label(String)
354 case anonymous(Int)
355 }
356 var n = 0
357 var translations: [String: String] = [:]
358 var definitions: [(key: Key, text: String)] = []
359 let collected = collectFootnoteReferences(anonymous: true)
360 let markers = collected.map { marker($0.begin) }
361 var keys: [Key] = []
362 for (reference, m) in zip(collected, markers) {
363 let new: String
364 let key: Key
365 if let label = reference.label {
366 if let known = translations[label] {
367 new = known
368 } else {
369 n += 1
370 new = String(n)
371 translations[label] = new
372 }
373 key = .label(label)
374 } else {
375 n += 1
376 new = String(n)
377 key = .anonymous(n)
378 }
379 keys.append(key)
380 point = m.position
381 setFootnoteLabel(new)
382 if let contents = reference.contents {
383 let size = contents.count
384 let removed = substring(point, point + size + 1)
385 deleteRegion(point, point + size + 1)
386 let text = "[fn:\(new)] " + String(removed.dropFirst()).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r"))
387 definitions.insert((key, text), at: 0)
388 }
389 }
390 markers.forEach(release)
391 definitions += collectFootnoteDefinitions(delete: true).map { (Key.label($0.label), $0.text) }
392 clearFootnoteSection()
393 var inserted: [Key] = []
394 for key in keys where !inserted.contains(key) {
395 inserted.append(key)
396 let stored = definitions.first { $0.key == key }?.text
397 let text: String
398 switch key {
399 case .anonymous(let number):
400 text = stored ?? "[fn:\(number)] DEFINITION NOT FOUND."
401 case .label(let label):
402 let new = translations[label]!
403 if let stored, let match = stored.firstMatch(of: /^\[fn:(.*?)\]/) {
404 text = stored.replacingCharacters(in: match.1.startIndex..<match.1.endIndex, with: new)
405 } else {
406 text = stored ?? "[fn:\(new)] DEFINITION NOT FOUND."
407 }
408 }
409 insert("\n" + text + "\n")
410 }
411 for definition in definitions where !inserted.contains(definition.key) {
412 n += 1
413 let number = n
414 let text = definition.text.replacingOccurrences(of: "(?m)^\\[fn:[-_\\w]+\\]", with: "[fn:\(number)]", options: .regularExpression)
415 insert("\n" + text + "\n")
416 }
417 }
418
419 /// `org-footnote-delete`: the message to show.
420 func deleteFootnote() throws -> String {
421 let label: String
422 if let reference = footnoteReferenceAtPoint() {
423 guard let l = reference.label else {
424 deleteRegion(reference.begin, reference.end)
425 return "Anonymous footnote removed"
426 }
427 label = l
428 } else if let definition = footnoteDefinitionAtPoint() {
429 label = definition.label
430 } else {
431 throw UserError(message: "Don't know which footnote to remove")
432 }
433 var references = 0
434 while let reference = footnoteReferences().first(where: { $0.label == label }) {
435 deleteRegion(reference.begin, reference.end)
436 references += 1
437 }
438 var definitions = 0
439 while let definition = footnoteDefinitions().first(where: { $0.label == label }) {
440 let begin = saveExcursion { () -> Int in
441 point = definition.begin
442 skipBackward([32, 13, 9, 10])
443 if bobp { return point }
444 forwardLine(1)
445 return point
446 }
447 deleteRegion(begin, definition.end)
448 definitions += 1
449 }
450 return "\(definitions) definition(s) of and \(references) reference(s) of footnote \(label) removed"
451 }
452}
453
454/// C-c C-x f: `org-footnote-action`. On a reference, goes to its definition; on a definition's
455/// label, back to a reference; elsewhere inserts a new footnote, or where one can't go, offers
456/// the footnote menu.
457public struct FootnoteAction: OrgCommand {
458 let special: Bool
459 public init(special: Bool = false) { self.special = special }
460 public var id: String { special ? "org.footnote.menu" : "org.footnote.action" }
461 public var title: String { special ? "Footnote Menu" : "Footnote Action" }
462 public func applies(in context: EditContext) -> Bool { true }
463
464 static let menu = FastSelection(items: [
465 .option(key: "s", name: "sort"), .option(key: "r", name: "renumber fn:N"), .option(key: "S", name: "renumber and sort"),
466 .option(key: "n", name: "normalize"), .option(key: "d", name: "delete"),
467 ], multiple: false)
468
469 public func run(in context: EditContext) -> CommandStep {
470 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
471 if !special {
472 if let reference = buffer.footnoteReferenceAtPoint() {
473 guard let label = reference.label else { return move(context, to: reference.contents!.lowerBound) }
474 if let start = buffer.footnoteDefinitionStart(label) {
475 buffer.gotoFootnoteDefinition(label, at: start)
476 return move(context, to: buffer.point, message: Self.definitionMessage)
477 }
478 guard let answer = context.answers["create"] else {
479 return .prompt(Prompt(key: "create", message: "No definition for \(label). Create one?", choices: ["yes", "no"]))
480 }
481 guard answer == "yes" else { return .failed("Quit") }
482 return commitBuffer(context) { buffer in
483 buffer.gotoFootnoteDefinition(label, at: buffer.createFootnoteDefinition(label))
484 }
485 }
486 if let definition = buffer.footnoteDefinitions().first(where: { $0.begin <= context.caret && context.caret < ($0.contentsBegin ?? $0.end) }) {
487 return commitBuffer(context) { try $0.gotoPreviousFootnoteReference(definition.label) }
488 }
489 if buffer.footnoteReferenceAllowed() { return commitBuffer(context) { try $0.newFootnote() } }
490 }
491 guard let key = context.answers["footnote"] else {
492 return .prompt(Prompt(key: "footnote", message: "Footnotes: [s]ort | [r]enumber fn:N | [S]=r+s | [n]ormalize | [d]elete", fast: Self.menu))
493 }
494 var message: String?
495 let step = commitBuffer(context) { buffer in
496 let caret = buffer.marker(buffer.point)
497 defer {
498 buffer.point = caret.position
499 buffer.release(caret)
500 }
501 switch key {
502 case "s": buffer.sortFootnotes()
503 case "r": buffer.renumberFootnotes()
504 case "S":
505 buffer.renumberFootnotes()
506 buffer.sortFootnotes()
507 case "n": buffer.normalizeFootnotes()
508 case "d": message = try buffer.deleteFootnote()
509 default: throw EmacsBuffer.UserError(message: "No such footnote command \(key)")
510 }
511 }
512 guard case .commit(let result) = step, let message else { return step }
513 return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, effects: result.effects + [.message(message)]))
514 }
515
516 static let definitionMessage = "Edit definition and go back with `C-c C-c' or `C-c C-x f' on its label."
517
518 private func move(_ context: EditContext, to offset: Int, message: String? = nil) -> CommandStep {
519 .commit(EditResult(baseRevision: context.revision, edits: [], selection: [offset..<offset], effects: message.map { [.message($0)] } ?? []))
520 }
521}