Sources/OrgSwiftUI/OrgLayout.swift
87 lines · 2805 bytes
1import OrgSwift
2import SwiftUI
3
4/// A document prepared for display: inline runs already styled, footnotes already numbered,
5/// image URLs already resolved, code already highlighted.
6///
7/// The walk happens once, ahead of `body`, because three things it does must not happen per
8/// layout pass — footnote numbering is sequential and document-wide, syntax highlighting is
9/// expensive, and both would otherwise re-run every time SwiftUI recomputes a view.
10struct OrgLayout {
11 var blocks: [OrgBlock] = []
12 var footnotes: [OrgFootnote] = []
13}
14
15struct OrgFootnote: Identifiable {
16 var number: Int
17 var text: AttributedString
18 var id: Int { number }
19}
20
21/// A block ready to render. Mirrors ``OrgElement`` minus the cases that resolve away:
22/// footnote definitions move to ``OrgLayout/footnotes``, and an export block for another
23/// backend drops entirely.
24indirect enum OrgBlock: Identifiable {
25 case heading(text: AttributedString, level: Int)
26 case paragraph(AttributedString)
27 case code(AttributedString)
28 case quote([OrgBlock])
29 case center([OrgBlock])
30 case verse([AttributedString])
31 case special(name: String, blocks: [OrgBlock])
32 case list(OrgListLayout)
33 case table(OrgTableLayout)
34 case image(source: URL, alt: String, caption: AttributedString?)
35 case badges([OrgBadge])
36 case captioned(caption: AttributedString?, blocks: [OrgBlock])
37 case rule
38
39 /// Positional identity. Blocks are rebuilt wholesale when the source changes, so their
40 /// index in the layout is stable for as long as the layout is.
41 var id: String { String(describing: self).prefix(64).description }
42}
43
44/// One image in a badge row, with the link it stands for when it has one.
45struct OrgBadge: Identifiable {
46 var source: URL
47 var alt: String
48 var link: URL?
49 var id: URL { source }
50}
51
52struct OrgListLayout {
53 var kind: OrgListKind
54 var items: [Item]
55
56 struct Item: Identifiable {
57 var checkbox: OrgCheckbox?
58 var term: AttributedString?
59 /// The item's body in source order — usually one paragraph, but an item may hold a
60 /// nested list, a source block, or a table.
61 var blocks: [OrgBlock]
62 var id = UUID()
63 }
64}
65
66struct OrgTableLayout {
67 /// Cell rows only; org's `|---|` rule rows are structure, carried by ``headerRowCount``.
68 var rows: [Row]
69 var headerRowCount: Int
70 var alignments: [OrgAlignment?]
71
72 struct Row: Identifiable {
73 var cells: [Cell]
74 var id = UUID()
75 }
76
77 struct Cell: Identifiable {
78 var text: AttributedString
79 var id = UUID()
80 }
81
82 var columnCount: Int { rows.map(\.cells.count).max() ?? 0 }
83
84 func alignment(column: Int) -> OrgAlignment? {
85 column < alignments.count ? alignments[column] : nil
86 }
87}