krz/org-swift

A dependency-free Swift library that renders org-mode to sanitized HTML. html library org-mode swift

Sources/OrgSwiftUI/OrgListView.swift

v0.3.0
org-swift/Sources/OrgSwiftUI/OrgListView.swift history · blame · raw

88 lines · 3201 bytes

 1import OrgSwift
 2import SwiftUI
 3
 4/// A list, recursing into its own sublists.
 5///
 6/// SwiftUI has no list-layout primitive, so the marker sits in its own column and the content
 7/// hangs beside it — wrapped lines line up under the text, not under the bullet.
 8struct OrgListView: View {
 9    let list: OrgListLayout
10    /// Ordered numbering restarts at each nesting level, as in org.
11    var body: some View {
12        VStack(alignment: .leading, spacing: 6) {
13            ForEach(Array(list.items.enumerated()), id: \.element.id) { index, item in
14                if list.kind == .description {
15                    descriptionItem(item)
16                } else {
17                    markedItem(item, marker: marker(at: index))
18                }
19            }
20        }
21    }
22
23    private func marker(at index: Int) -> String {
24        list.kind == .ordered ? "\(index + 1)." : "•"
25    }
26
27    private func markedItem(_ item: OrgListLayout.Item, marker: String) -> some View {
28        HStack(alignment: .firstTextBaseline, spacing: 8) {
29            Text(marker)
30                .monospacedDigit()
31                .foregroundStyle(.secondary)
32            VStack(alignment: .leading, spacing: 6) {
33                if let checkbox = item.checkbox {
34                    HStack(alignment: .firstTextBaseline, spacing: 6) {
35                        Image(systemName: Self.symbol(for: checkbox))
36                            .foregroundStyle(checkbox == .on ? Color.accentColor : .secondary)
37                            .accessibilityLabel(Self.label(for: checkbox))
38                        paragraphs(item)
39                    }
40                } else {
41                    paragraphs(item)
42                }
43                if let sublist = item.sublist {
44                    OrgListView(list: sublist)
45                }
46            }
47        }
48    }
49
50    /// `term :: definition` — the term above, the definition indented under it.
51    private func descriptionItem(_ item: OrgListLayout.Item) -> some View {
52        VStack(alignment: .leading, spacing: 2) {
53            if let term = item.term {
54                Text(term).fixedSize(horizontal: false, vertical: true)
55            }
56            paragraphs(item).padding(.leading, 16)
57            if let sublist = item.sublist {
58                OrgListView(list: sublist).padding(.leading, 16)
59            }
60        }
61    }
62
63    private func paragraphs(_ item: OrgListLayout.Item) -> some View {
64        VStack(alignment: .leading, spacing: 6) {
65            ForEach(Array(item.paragraphs.enumerated()), id: \.offset) { _, paragraph in
66                Text(paragraph)
67                    .fixedSize(horizontal: false, vertical: true)
68                    .frame(maxWidth: .infinity, alignment: .leading)
69            }
70        }
71    }
72
73    private static func symbol(for checkbox: OrgCheckbox) -> String {
74        switch checkbox {
75        case .on: "checkmark.square.fill"
76        case .off: "square"
77        case .partial: "minus.square"
78        }
79    }
80
81    private static func label(for checkbox: OrgCheckbox) -> String {
82        switch checkbox {
83        case .on: "Done"
84        case .off: "Not done"
85        case .partial: "Partly done"
86        }
87    }
88}