Compare commits

...

8 Commits

16 changed files with 276 additions and 36 deletions

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import Combine import Combine
public protocol NavigationManagerDelegate: class { public protocol NavigationManagerDelegate: AnyObject {
func loadNonGeminiURL(_ url: URL) func loadNonGeminiURL(_ url: URL)
} }

View File

@ -0,0 +1,34 @@
//
// ActivityItemSource.swift
// Gemini-iOS
//
// Created by Shadowfacts on 6/15/21.
//
import UIKit
import GeminiFormat
import LinkPresentation
class ActivityItemSource: NSObject, UIActivityItemSource {
let document: Document
init(document: Document) {
self.document = document
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return document.url
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return document.url
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.url = document.url
metadata.title = document.title
return metadata
}
}

View File

@ -10,7 +10,7 @@ import UIKit
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
static let defaultHomepage = URL(string: "gemini://gemini.circumlunar.space/")!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SymbolCache.load() SymbolCache.load()

View File

@ -303,7 +303,8 @@ class BrowserNavigationController: UIViewController {
} }
private func showShareSheet(_ source: UIView) { private func showShareSheet(_ source: UIView) {
let vc = UIActivityViewController(activityItems: [navigator.currentURL], applicationActivities: nil) guard let doc = currentBrowserVC.document else { return }
let vc = UIActivityViewController(activityItems: [ActivityItemSource(document: doc)], applicationActivities: [SetHomepageActivity()])
vc.popoverPresentationController?.sourceView = source vc.popoverPresentationController?.sourceView = source
present(vc, animated: true) present(vc, animated: true)
} }

View File

@ -12,6 +12,7 @@ import GeminiProtocol
import GeminiFormat import GeminiFormat
import GeminiRenderer import GeminiRenderer
import SafariServices import SafariServices
import Combine
class BrowserWebViewController: UIViewController { class BrowserWebViewController: UIViewController {
@ -32,6 +33,8 @@ class BrowserWebViewController: UIViewController {
private var loaded = false private var loaded = false
private var loadedFallback = false private var loadedFallback = false
private var cancellables = Set<AnyCancellable>()
private var errorStack: UIStackView! private var errorStack: UIStackView!
private var errorMessageLabel: UILabel! private var errorMessageLabel: UILabel!
private var activityIndicator: UIActivityIndicatorView! private var activityIndicator: UIActivityIndicatorView!
@ -51,25 +54,7 @@ class BrowserWebViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
let documentURL = self.url configureRenderer()
renderer.linkPrefix = { (url: URL) -> String in
let symbolClass: String
if url.scheme == "gemini" {
if url.host == documentURL.host {
symbolClass = "arrow-right"
} else {
symbolClass = "link"
}
} else if url.scheme == "http" || url.scheme == "https" {
symbolClass = "safari"
} else if url.scheme == "mailto" {
symbolClass = "envelope"
} else {
symbolClass = "arrow-up-left-square"
}
return "<span class=\"symbol \(symbolClass)\" aria-hidden=\"true\"></span>"
}
view.backgroundColor = .systemBackground view.backgroundColor = .systemBackground
@ -122,6 +107,14 @@ class BrowserWebViewController: UIViewController {
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
]) ])
Preferences.shared.$showLinkIcons
.sink { [weak self] newVal in
guard let self = self, let doc = self.document else { return }
self.configureRenderer(showLinkIcons: newVal)
self.renderDocument(doc)
}
.store(in: &cancellables)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -130,6 +123,31 @@ class BrowserWebViewController: UIViewController {
loadDocument() loadDocument()
} }
private func configureRenderer(showLinkIcons: Bool = Preferences.shared.showLinkIcons) {
if showLinkIcons {
let documentURL = self.url
renderer.linkPrefix = { (url: URL) -> String in
let symbolClass: String
if url.scheme == "gemini" {
if url.host == documentURL.host {
symbolClass = "arrow-right"
} else {
symbolClass = "link"
}
} else if url.scheme == "http" || url.scheme == "https" {
symbolClass = "safari"
} else if url.scheme == "mailto" {
symbolClass = "envelope"
} else {
symbolClass = "arrow-up-left-square"
}
return "<span class=\"symbol \(symbolClass)\" aria-hidden=\"true\"></span>"
}
} else {
renderer.linkPrefix = nil
}
}
func reload() { func reload() {
loaded = false loaded = false
loadedFallback = false loadedFallback = false

View File

@ -0,0 +1,96 @@
//
// HomepagePrefView.swift
// Gemini-iOS
//
// Created by Shadowfacts on 6/15/21.
//
import SwiftUI
struct HomepagePrefView: View {
@State private var url: URL? = Preferences.shared.homepage
@State private var focus = false
var body: some View {
List {
// use a custom binding to allow the state variable to be nil temporarily, and update the pref when not
HomepagePrefTextField(value: Binding(get: {
url
}, set: { (newVal) in
self.url = newVal
if let newVal = newVal {
Preferences.shared.homepage = newVal
}
}), focus: $focus)
}
.navigationBarTitle("Homepage")
.onAppear {
focus = true
}
.onDisappear {
// if the text field was empty when we disappeared, reset to the default homepage
if url == nil {
Preferences.shared.homepage = AppDelegate.defaultHomepage
}
} }
}
struct HomepagePrefTextField: UIViewRepresentable {
@Binding private var value: URL?
@Binding private var focus: Bool
init(value: Binding<URL?>, focus: Binding<Bool>) {
self._value = value
self._focus = focus
}
func makeUIView(context: Context) -> UITextField {
let field = UITextField()
field.addTarget(context.coordinator, action: #selector(Coordinator.textChanged), for: .editingChanged)
field.clearButtonMode = .whileEditing
field.placeholder = AppDelegate.defaultHomepage.absoluteString
// fix for text field expanding horizontally when text grows
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return field
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = value?.absoluteString
context.coordinator.binding = $value
if focus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
focus = false
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(binding: $value)
}
class Coordinator: NSObject {
var binding: Binding<URL?>
init(binding: Binding<URL?>) {
self.binding = binding
}
@objc func textChanged(_ textField: UITextField) {
if let text = textField.text,
!text.isEmpty {
if let url = URL(string: text) {
binding.wrappedValue = url
}
} else {
binding.wrappedValue = nil
}
}
}
}
struct HomepagePrefView_Previews: PreviewProvider {
static var previews: some View {
HomepagePrefView()
}
}

View File

@ -77,7 +77,9 @@ class NavigationBarView: UIView {
@objc private func commitURL() { @objc private func commitURL() {
textField.resignFirstResponder() textField.resignFirstResponder()
if let text = textField.text, var components = URLComponents(string: text) { if let text = textField.text,
!text.isEmpty,
var components = URLComponents(string: text) {
if components.scheme == nil { if components.scheme == nil {
components.scheme = "gemini" components.scheme = "gemini"
} }

View File

@ -34,7 +34,14 @@ class Preferences: Codable, ObservableObject {
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
if let stored = try container.decodeIfPresent(URL.self, forKey: .homepage) {
homepage = stored
}
theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme) theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
if let stored = try container.decodeIfPresent(Bool.self, forKey: .showLinkIcons) {
showLinkIcons = stored
}
useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari) useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
useReaderMode = try container.decode(Bool.self, forKey: .useReaderMode) useReaderMode = try container.decode(Bool.self, forKey: .useReaderMode)
@ -43,19 +50,28 @@ class Preferences: Codable, ObservableObject {
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(homepage, forKey: .homepage)
try container.encode(theme, forKey: .theme) try container.encode(theme, forKey: .theme)
try container.encode(showLinkIcons, forKey: .showLinkIcons)
try container.encode(useInAppSafari, forKey: .useInAppSafari) try container.encode(useInAppSafari, forKey: .useInAppSafari)
try container.encode(useReaderMode, forKey: .useReaderMode) try container.encode(useReaderMode, forKey: .useReaderMode)
} }
@Published var homepage = AppDelegate.defaultHomepage
@Published var theme = UIUserInterfaceStyle.unspecified @Published var theme = UIUserInterfaceStyle.unspecified
@Published var showLinkIcons = true
@Published var useInAppSafari = false @Published var useInAppSafari = false
@Published var useReaderMode = false @Published var useReaderMode = false
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case homepage
case theme case theme
case showLinkIcons
case useInAppSafari case useInAppSafari
case useReaderMode case useReaderMode

View File

@ -15,6 +15,8 @@ struct PreferencesView: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
List { List {
untitledSection
appearanceSection appearanceSection
safariSection safariSection
@ -23,7 +25,7 @@ struct PreferencesView: View {
.insetOrGroupedListStyle() .insetOrGroupedListStyle()
.navigationBarItems(trailing: doneButton) .navigationBarItems(trailing: doneButton)
} }
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(.stack)
.onDisappear { .onDisappear {
Preferences.save() Preferences.save()
} }
@ -38,6 +40,15 @@ struct PreferencesView: View {
.hoverEffect(.highlight) .hoverEffect(.highlight)
} }
private var untitledSection: some View {
Section {
NavigationLink(destination: HomepagePrefView()) {
Text("Homepage")
}
}
}
private var appearanceSection: some View { private var appearanceSection: some View {
Section(header: Text("Appearance")) { Section(header: Text("Appearance")) {
Picker(selection: $preferences.theme, label: Text("Theme")) { Picker(selection: $preferences.theme, label: Text("Theme")) {
@ -45,6 +56,8 @@ struct PreferencesView: View {
Text("Always Light").tag(UIUserInterfaceStyle.light) Text("Always Light").tag(UIUserInterfaceStyle.light)
Text("Always Dark").tag(UIUserInterfaceStyle.dark) Text("Always Dark").tag(UIUserInterfaceStyle.dark)
} }
Toggle("Show Link Icons", isOn: $preferences.showLinkIcons)
} }
} }
@ -62,9 +75,9 @@ fileprivate extension View {
@ViewBuilder @ViewBuilder
func insetOrGroupedListStyle() -> some View { func insetOrGroupedListStyle() -> some View {
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
self.listStyle(InsetGroupedListStyle()) self.listStyle(.insetGrouped)
} else { } else {
self.listStyle(GroupedListStyle()) self.listStyle(.grouped)
} }
} }
} }

View File

@ -32,7 +32,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if ProcessInfo.processInfo.environment.keys.contains("DEFAULT_URL") { if ProcessInfo.processInfo.environment.keys.contains("DEFAULT_URL") {
initialURL = URL(string: ProcessInfo.processInfo.environment["DEFAULT_URL"]!)! initialURL = URL(string: ProcessInfo.processInfo.environment["DEFAULT_URL"]!)!
} else { } else {
initialURL = URL(string: "gemini://gemini.circumlunar.space/")! initialURL = Preferences.shared.homepage
} }
} }

View File

@ -0,0 +1,38 @@
//
// SetHomepageActivity.swift
// Gemini-iOS
//
// Created by Shadowfacts on 6/15/21.
//
import UIKit
class SetHomepageActivity: UIActivity {
override class var activityCategory: UIActivity.Category {
return .action
}
override var activityTitle: String? {
return "Set as Homepage"
}
override var activityImage: UIImage? {
// large size more closely matches system activity images
return UIImage(systemName: "house", withConfiguration: UIImage.SymbolConfiguration(scale: .large))
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
for case let url as URL in activityItems {
return Preferences.shared.homepage != url
}
return false
}
override func prepare(withActivityItems activityItems: [Any]) {
for case let url as URL in activityItems {
Preferences.shared.homepage = url
break
}
}
}

View File

@ -36,6 +36,9 @@
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; }; D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */; };
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; }; D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62664A824BBF26A00DF9B88 /* GeminiFormat.framework */; };
D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; }; D62664FA24BC12BC00DF9B88 /* DocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62664F924BC12BC00DF9B88 /* DocumentTests.swift */; };
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40A267996FF004E32B1 /* ActivityItemSource.swift */; };
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */; };
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */; };
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; }; D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; }; D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; }; D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
@ -303,6 +306,9 @@
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; }; D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; };
D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; }; D62664ED24BC0BCE00DF9B88 /* MaybeLazyVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeLazyVStack.swift; sourceTree = "<group>"; };
D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; }; D62664F924BC12BC00DF9B88 /* DocumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTests.swift; sourceTree = "<group>"; };
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; };
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepagePrefView.swift; sourceTree = "<group>"; };
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetHomepageActivity.swift; sourceTree = "<group>"; };
D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; }; D664673524BD07F700B0B741 /* RenderingBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlock.swift; sourceTree = "<group>"; };
D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; }; D664673724BD086F00B0B741 /* RenderingBlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderingBlockView.swift; sourceTree = "<group>"; };
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; }; D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
@ -593,6 +599,9 @@
D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */, D6BC9AD6258FC8B3008652BC /* TableOfContentsView.swift */,
D691A64D25217C6F00348C4B /* Preferences.swift */, D691A64D25217C6F00348C4B /* Preferences.swift */,
D691A66625217FD800348C4B /* PreferencesView.swift */, D691A66625217FD800348C4B /* PreferencesView.swift */,
D653F40C26799F2F004E32B1 /* HomepagePrefView.swift */,
D653F40A267996FF004E32B1 /* ActivityItemSource.swift */,
D653F40E2679A0AB004E32B1 /* SetHomepageActivity.swift */,
D688F618258AD231003A0A73 /* Resources */, D688F618258AD231003A0A73 /* Resources */,
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */, D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */, D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
@ -1111,11 +1120,14 @@
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */, D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */, D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */, D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
D653F40B267996FF004E32B1 /* ActivityItemSource.swift in Sources */,
D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */, D6BC9AB3258E8E13008652BC /* ToolbarView.swift in Sources */,
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */, D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
D653F40F2679A0AB004E32B1 /* SetHomepageActivity.swift in Sources */,
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */, D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */, D6BC9AD7258FC8B3008652BC /* TableOfContentsView.swift in Sources */,
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */, D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
D653F40D26799F2F004E32B1 /* HomepagePrefView.swift in Sources */,
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */, D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */, D6BC9ABC258E9862008652BC /* NavigationBarView.swift in Sources */,
); );

View File

@ -2,7 +2,7 @@
"object": { "object": {
"pins": [ "pins": [
{ {
"package": "HTMLEntities", "package": "swift-html-entities",
"repositoryURL": "https://github.com/Kitura/swift-html-entities", "repositoryURL": "https://github.com/Kitura/swift-html-entities",
"state": { "state": {
"branch": null, "branch": null,

View File

@ -44,8 +44,6 @@ public struct GeminiParser {
// URL(string:relativeTo:) does not handle // meaning the same protocol as the base URL // URL(string:relativeTo:) does not handle // meaning the same protocol as the base URL
urlString = baseURL.scheme! + ":" + urlString urlString = baseURL.scheme! + ":" + urlString
} }
// todo: if the URL initializer fails, should there be a .link line with a nil URL?
let url = URL(string: urlString, relativeTo: baseURL)!.absoluteURL
let text: String? let text: String?
if textStart < line.endIndex { if textStart < line.endIndex {
@ -54,7 +52,18 @@ public struct GeminiParser {
text = nil text = nil
} }
doc.lines.append(.link(url, text: text)) if let url = URL(string: urlString, relativeTo: baseURL)?.absoluteURL {
doc.lines.append(.link(url, text: text))
} else {
let str: String
if let text = text {
// todo: localize me?
str = "\(text): \(urlString)"
} else {
str = urlString
}
doc.lines.append(.text(str))
}
} else if line.starts(with: "#") { } else if line.starts(with: "#") {
let level: Document.HeadingLevel let level: Document.HeadingLevel
if line.starts(with: "###") { if line.starts(with: "###") {

View File

@ -79,6 +79,7 @@ public class GeminiDataTask {
self.completion(.failure(.connectionError(error))) self.completion(.failure(.connectionError(error)))
} else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message, } else if let message = context?.protocolMetadata(definition: GeminiProtocol.definition) as? NWProtocolFramer.Message,
let header = message.geminiResponseHeader { let header = message.geminiResponseHeader {
guard isComplete else { fatalError() }
let response = GeminiResponse(header: header, body: data) let response = GeminiResponse(header: header, body: data)
self.completion(.success(response)) self.completion(.success(response))
} }

View File

@ -88,12 +88,12 @@ class GeminiProtocol: NWProtocolFramerImplementation {
let header = GeminiResponseHeader(status: statusCode, meta: meta) let header = GeminiResponseHeader(status: statusCode, meta: meta)
let message = NWProtocolFramer.Message(geminiResponseHeader: header) let message = NWProtocolFramer.Message(geminiResponseHeader: header)
// What does the return value of deliverInputNoCopy mean, you ask? Why, I have no idea // Deliver all the input (the response body) to the client without copying.
// It always returns true for a length of zero, so following the sample code and looping
// infinitely until it returns false causes an infinite loop.
// Additionally, calling deliverInput with an empty Data() causes an error inside Network.framework.
// So, we just ignore the result since it doesn't seem to cause any problems ¯\_()_/¯
_ = framer.deliverInputNoCopy(length: statusCode.isSuccess ? .max : 0, message: message, isComplete: true) _ = framer.deliverInputNoCopy(length: statusCode.isSuccess ? .max : 0, message: message, isComplete: true)
// Just in case, set the framer to pass-through input so it never invokes this method again.
// todo: this should work according to an apple engineer, but the request seems to hang forever on a real device
// sometimes works fine when stepping through w/ debugger => race condition?
// framer.passThroughInput()
return 0 return 0
} }