Sources/OrgSwiftUI/OrgBlockView.swift
108 lines · 3475 bytes
1import OrgSwift
2import SwiftUI
3
4/// Renders prepared blocks. All the org work happened in ``OrgLayoutBuilder``; this is
5/// layout only.
6struct OrgBlockListView: View {
7 let blocks: [OrgBlock]
8
9 var body: some View {
10 VStack(alignment: .leading, spacing: 12) {
11 ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in
12 OrgBlockView(block: block)
13 }
14 }
15 .frame(maxWidth: .infinity, alignment: .leading)
16 }
17}
18
19struct OrgBlockView: View {
20 let block: OrgBlock
21
22 var body: some View {
23 switch block {
24 case .heading(let text, let level):
25 Text(text)
26 .padding(.top, level <= 2 ? 8 : 2)
27 .fixedSize(horizontal: false, vertical: true)
28
29 case .paragraph(let text):
30 Text(text)
31 .fixedSize(horizontal: false, vertical: true)
32
33 case .code(let code):
34 ScrollView(.horizontal) {
35 Text(code)
36 .textSelection(.enabled)
37 .padding(10)
38 }
39 .background(
40 Color.secondary.opacity(0.12),
41 in: RoundedRectangle(cornerRadius: 4)
42 )
43
44 case .quote(let blocks):
45 HStack(spacing: 10) {
46 RoundedRectangle(cornerRadius: 2)
47 .fill(.tertiary)
48 .frame(width: 3)
49 OrgBlockListView(blocks: blocks)
50 .foregroundStyle(.secondary)
51 }
52 .fixedSize(horizontal: false, vertical: true)
53
54 case .center(let blocks):
55 OrgBlockListView(blocks: blocks)
56 .multilineTextAlignment(.center)
57 .frame(maxWidth: .infinity, alignment: .center)
58
59 case .verse(let lines):
60 // Verse keeps its line breaks; that is the whole point of the block.
61 VStack(alignment: .leading, spacing: 0) {
62 ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
63 Text(line).fixedSize(horizontal: false, vertical: true)
64 }
65 }
66
67 case .special(_, let blocks):
68 OrgBlockListView(blocks: blocks)
69
70 case .list(let list):
71 OrgListView(list: list)
72
73 case .table(let table):
74 OrgTableView(table: table)
75
76 case .image(let source, let alt, let caption):
77 VStack(alignment: .leading, spacing: 4) {
78 AsyncImage(url: source) { phase in
79 switch phase {
80 case .success(let image):
81 image.resizable().scaledToFit()
82 case .failure:
83 Text(alt.isEmpty ? source.lastPathComponent : alt)
84 .font(.footnote)
85 .foregroundStyle(.secondary)
86 default:
87 ProgressView()
88 }
89 }
90 .accessibilityLabel(alt)
91 if let caption {
92 Text(caption).foregroundStyle(.secondary)
93 }
94 }
95
96 case .captioned(let caption, let blocks):
97 VStack(alignment: .leading, spacing: 4) {
98 OrgBlockListView(blocks: blocks)
99 if let caption {
100 Text(caption).foregroundStyle(.secondary)
101 }
102 }
103
104 case .rule:
105 Divider()
106 }
107 }
108}