summaryrefslogtreecommitdiff
path: root/Hutch/Views/More/ManPageDetailView.swift
blob: 96a92fb5288478ccd2e9114e5813f8db46c94ec9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import SwiftUI

/// Fetches and renders a single man.sr.ht page.
/// Internal man.sr.ht links update the current URL in-place rather than
/// opening the browser, so the user can follow wiki links without leaving
/// the view.
struct ManPageDetailView: View {
    let initialURL: URL

    @Environment(\.colorScheme) private var colorScheme
    @State private var currentURL: URL
    @State private var page: ManPage?
    @State private var isLoading = false
    @State private var error: String?

    init(url: URL) {
        initialURL = url
        _currentURL = State(initialValue: url)
    }

    var body: some View {
        ScrollView {
            if isLoading {
                SRHTLoadingStateView(message: "Loading page…")
                    .padding(.top, 40)
            } else if let error {
                SRHTErrorStateView(
                    title: "Couldn't Load Page",
                    message: error,
                    retryAction: { await loadPage() }
                )
                .padding()
            } else if let page {
                HTMLWebView(
                    html: page.contentHTML,
                    colorScheme: colorScheme,
                    style: .readme,
                    baseURL: page.url,
                    onInterceptURL: { url in
                        guard let destinationURL = normalizedManPageURL(for: url) else {
                            return false
                        }
                        currentURL = destinationURL
                        return true
                    }
                )
                .padding()
            }
        }
        .navigationTitle(page?.title ?? "Documentation")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                if let page {
                    Link(destination: page.url) {
                        Image(systemName: "safari")
                    }
                }
            }
        }
        .task(id: currentURL) {
            await loadPage()
        }
    }

    private func loadPage() async {
        isLoading = true
        error = nil

        do {
            page = try await ManPageService.fetch(url: currentURL)
        } catch {
            self.error = error.localizedDescription
        }

        isLoading = false
    }

    private func normalizedManPageURL(for url: URL) -> URL? {
        if ManPageService.isTrustedDocumentationURL(url) {
            return url
        }

        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let scheme = components.scheme?.lowercased(),
              scheme == "about" || scheme == "file" else {
            return nil
        }

        let rawPath = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        guard !rawPath.isEmpty else {
            if currentURL.host?.lowercased() == "srht.site" {
                return ManPageService.pagesBaseURL
            }
            return ManPageService.baseURL
        }

        if currentURL.host?.lowercased() == "srht.site" {
            return URL(string: "https://srht.site/\(rawPath)") ?? ManPageService.pagesBaseURL
        }
        return URL(string: "https://man.sr.ht/\(rawPath)/") ?? ManPageService.baseURL
    }
}