Replace SwiftUI renderer with HTML/WKWebView
This commit is contained in:
parent
9d1d8828a0
commit
2d60f733c3
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// BrowserNavigationController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import Combine
|
||||
|
||||
class BrowserNavigationController: UINavigationController {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
var poppedViewControllers = [UIViewController]()
|
||||
var skipResetPoppedOnNextPush = false
|
||||
|
||||
private var interactivePushTransition: InteractivePushTransition!
|
||||
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
navigator.$currentURL
|
||||
.sink { [weak self] (newURL) in
|
||||
self?.pushViewController(BrowserWebViewController(navigator: navigator, url: newURL), animated: true)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||
}
|
||||
|
||||
override func popViewController(animated: Bool) -> UIViewController? {
|
||||
if let popped = super.popViewController(animated: animated) {
|
||||
poppedViewControllers.insert(popped, at: 0)
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||
if let popped = super.popToRootViewController(animated: animated) {
|
||||
poppedViewControllers = popped
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
||||
if let popped = super.popToViewController(viewController, animated: animated) {
|
||||
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
if skipResetPoppedOnNextPush {
|
||||
skipResetPoppedOnNextPush = false
|
||||
} else {
|
||||
self.poppedViewControllers = []
|
||||
}
|
||||
super.pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
func onWillShow() {
|
||||
self.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in
|
||||
if context.isCancelled {
|
||||
if self.interactivePushTransition.interactive {
|
||||
// when an interactive push gesture is cancelled, make sure to adding the VC that was being pushed back onto the popped stack so it doesn't disappear
|
||||
self.poppedViewControllers.insert(self.interactivePushTransition.pushingViewController!, at: 0)
|
||||
} else {
|
||||
// when an interactive pop gesture is cancelled (i.e. the user lifts their finger before it triggers),
|
||||
// the popViewController(animated:) method has already been called so the VC has already been added to the popped stack
|
||||
// so we make sure to remove it, otherwise there could be duplicate VCs on the navigation stasck
|
||||
self.poppedViewControllers.remove(at: 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
//
|
||||
// BrowserWebViewController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import BrowserCore
|
||||
import WebKit
|
||||
import GeminiProtocol
|
||||
import GeminiFormat
|
||||
import GeminiRenderer
|
||||
|
||||
class BrowserWebViewController: UIViewController {
|
||||
|
||||
let navigator: NavigationManager
|
||||
let url: URL
|
||||
private var task: GeminiDataTask?
|
||||
private let renderer = GeminiHTMLRenderer()
|
||||
|
||||
private var loaded = false
|
||||
|
||||
private var errorStack: UIStackView!
|
||||
private var errorMessageLabel: UILabel!
|
||||
private var activityIndicator: UIActivityIndicatorView!
|
||||
private var webView: WKWebView!
|
||||
|
||||
init(navigator: NavigationManager, url: URL) {
|
||||
self.navigator = navigator
|
||||
self.url = url
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
webView = WKWebView()
|
||||
webView.navigationDelegate = self
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(webView)
|
||||
NSLayoutConstraint.activate([
|
||||
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
webView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
let errorTitle = UILabel()
|
||||
errorTitle.text = "An error occurred"
|
||||
errorTitle.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline).withDesign(.serif)!, size: 0)
|
||||
errorTitle.numberOfLines = 0
|
||||
errorMessageLabel = UILabel()
|
||||
errorMessageLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withDesign(.serif)!, size: 0)
|
||||
errorMessageLabel.numberOfLines = 0
|
||||
errorStack = UIStackView(arrangedSubviews: [
|
||||
errorTitle,
|
||||
errorMessageLabel,
|
||||
])
|
||||
errorStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
errorStack.axis = .vertical
|
||||
errorStack.alignment = .center
|
||||
errorStack.isHidden = true
|
||||
view.addSubview(errorStack)
|
||||
NSLayoutConstraint.activate([
|
||||
errorStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
errorStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
errorStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
|
||||
activityIndicator = UIActivityIndicatorView(style: .large)
|
||||
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||
activityIndicator.isHidden = true
|
||||
view.addSubview(activityIndicator)
|
||||
NSLayoutConstraint.activate([
|
||||
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
loadDocument()
|
||||
}
|
||||
|
||||
private func loadDocument() {
|
||||
guard !loaded else { return }
|
||||
|
||||
webView.isHidden = true
|
||||
errorStack.isHidden = true
|
||||
activityIndicator.isHidden = false
|
||||
activityIndicator.startAnimating()
|
||||
|
||||
let url = self.url
|
||||
task = try! GeminiDataTask(url: url) { (response) in
|
||||
self.task = nil
|
||||
self.loaded = true
|
||||
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
DispatchQueue.main.async {
|
||||
self.showError(message: error.localizedDescription)
|
||||
}
|
||||
case let .success(response):
|
||||
if response.status.isRedirect {
|
||||
if let redirect = URL(string: response.meta) {
|
||||
self.navigator.changeURL(redirect)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.showError(message: "Invalid redirect URL: '\(response.meta)'")
|
||||
}
|
||||
}
|
||||
} else if response.status.isSuccess,
|
||||
let text = response.bodyText {
|
||||
self.renderDocument(GeminiParser.parse(text: text, baseURL: url))
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.showError(message: "Unknown error: \(response.header)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
task!.resume()
|
||||
}
|
||||
|
||||
private func showError(message: String) {
|
||||
webView.isHidden = true
|
||||
errorStack.isHidden = false
|
||||
activityIndicator.isHidden = true
|
||||
activityIndicator.stopAnimating()
|
||||
|
||||
errorMessageLabel.text = message
|
||||
}
|
||||
|
||||
private func renderDocument(_ doc: Document) {
|
||||
let html = BrowserWebViewController.preamble + renderer.renderDocumentToHTML(doc) + BrowserWebViewController.postamble
|
||||
DispatchQueue.main.async {
|
||||
self.webView.isHidden = false
|
||||
self.errorStack.isHidden = true
|
||||
self.activityIndicator.isHidden = true
|
||||
self.activityIndicator.stopAnimating()
|
||||
|
||||
if let title = doc.title {
|
||||
self.navigationItem.title = title
|
||||
}
|
||||
|
||||
self.webView.loadHTMLString(html, baseURL: Bundle.main.bundleURL)
|
||||
}
|
||||
}
|
||||
|
||||
private static let preamble = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="stylesheet" href="\(Bundle.main.url(forResource: "browser", withExtension: "css")!.absoluteString)">
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
|
||||
private static let postamble = """
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
}
|
||||
|
||||
extension BrowserWebViewController: WKNavigationDelegate {
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
let url = navigationAction.request.url!
|
||||
if url.scheme == "file" {
|
||||
decisionHandler(.allow)
|
||||
} else {
|
||||
decisionHandler(.cancel)
|
||||
navigator.changeURL(url)
|
||||
loadDocument()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// InteractivePushTransition.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Allows interactively moving forward through the navigation stack after popping
|
||||
/// Based on https://github.com/NSExceptional/TBInteractivePushTransition
|
||||
class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
|
||||
|
||||
fileprivate let minimumPushVelocityThreshold: CGFloat = 700
|
||||
fileprivate let minimumPushDistanceThreshold: CGFloat = 0.5
|
||||
fileprivate let pushAnimationDuration: TimeInterval = 0.35
|
||||
|
||||
private(set) weak var navigationController: BrowserNavigationController!
|
||||
|
||||
private(set) weak var interactivePushGestureRecognizer: UIScreenEdgePanGestureRecognizer!
|
||||
|
||||
private(set) var interactive = false
|
||||
private(set) weak var pushingViewController: UIViewController?
|
||||
|
||||
init(navigationController: BrowserNavigationController) {
|
||||
super.init()
|
||||
|
||||
self.navigationController = navigationController
|
||||
|
||||
let interactivePushGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
||||
self.interactivePushGestureRecognizer = interactivePushGestureRecognizer
|
||||
|
||||
navigationController.delegate = self
|
||||
interactivePushGestureRecognizer.edges = .right
|
||||
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
|
||||
|
||||
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
||||
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
||||
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
|
||||
}
|
||||
|
||||
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {
|
||||
interactive = true
|
||||
|
||||
let velocity = recognizer.velocity(in: recognizer.view)
|
||||
let translation = recognizer.translation(in: recognizer.view)
|
||||
let dx = -translation.x / navigationController.view.bounds.width
|
||||
let vx = -velocity.x
|
||||
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let viewController = navigationController.poppedViewControllers.first {
|
||||
pushingViewController = viewController
|
||||
navigationController.poppedViewControllers.removeFirst()
|
||||
navigationController.skipResetPoppedOnNextPush = true
|
||||
navigationController.pushViewController(viewController, animated: true)
|
||||
} else {
|
||||
interactive = false
|
||||
}
|
||||
|
||||
case .changed:
|
||||
update(dx)
|
||||
|
||||
case .ended:
|
||||
if (dx > minimumPushDistanceThreshold || vx > minimumPushVelocityThreshold) {
|
||||
finish()
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
interactive = false
|
||||
|
||||
default:
|
||||
cancel()
|
||||
interactive = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension InteractivePushTransition: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||
self.navigationController.onWillShow()
|
||||
}
|
||||
|
||||
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
if operation == .push, interactive {
|
||||
return self
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
||||
if interactive {
|
||||
return self
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension InteractivePushTransition: UIViewControllerAnimatedTransitioning {
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
return pushAnimationDuration
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
let toView = transitionContext.view(forKey: .to)!
|
||||
let fromView = transitionContext.view(forKey: .from)!
|
||||
|
||||
let dimmingView = UIView()
|
||||
dimmingView.backgroundColor = .black
|
||||
dimmingView.alpha = 0
|
||||
dimmingView.frame = fromView.frame
|
||||
|
||||
transitionContext.containerView.addSubview(dimmingView)
|
||||
transitionContext.containerView.addSubview(toView)
|
||||
|
||||
// modify frame of presented view to go from x = <screen width> to x = 0
|
||||
var frame = fromView.frame
|
||||
frame.origin.x = frame.width
|
||||
toView.frame = frame
|
||||
frame.origin.x = 0
|
||||
|
||||
// we want it linear while interactive, but we want the "ease out" animation
|
||||
// if the user flicks the screen ahrd enough to finish the transition without interaction
|
||||
let options = interactive ? UIView.AnimationOptions.curveLinear : .curveEaseOut
|
||||
|
||||
let duration = transitionDuration(using: transitionContext)
|
||||
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
|
||||
// these magic numbers scientifically determined by repeatedly adjusting and comparing to the system animation
|
||||
let translationDistance = -frame.size.width * 0.3
|
||||
fromView.transform = CGAffineTransform(translationX: translationDistance, y: 0)
|
||||
toView.frame = frame
|
||||
dimmingView.alpha = 0.075
|
||||
}) { (finished) in
|
||||
fromView.transform = .identity
|
||||
dimmingView.removeFromSuperview()
|
||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
body {
|
||||
font-family: ui-serif;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: ui-monospace;
|
||||
overflow-wrap: normal;
|
||||
overflow-x: auto;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
line-height: 1;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p.link {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(0, 122, 255);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(10, 132, 255);
|
||||
}
|
||||
}
|
|
@ -40,13 +40,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
navigationManager.delegate = self
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = ContentView(navigator: navigationManager)
|
||||
// let contentView = ContentView(navigator: navigationManager)
|
||||
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
window.rootViewController = BrowserNavigationController(navigator: navigationManager)
|
||||
// window.rootViewController = UIHostingController(rootView: contentView)
|
||||
// window.rootViewController = BrowserViewController(navigator: navigationManager)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// TrackpadScrollGestureRecognizer.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TrackpadScrollGestureRecognizer: UIPanGestureRecognizer {
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.allowedScrollTypesMask = .all
|
||||
}
|
||||
|
||||
override func shouldReceive(_ event: UIEvent) -> Bool {
|
||||
return event.type == .scroll
|
||||
}
|
||||
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 52;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -40,6 +40,13 @@
|
|||
D664673624BD07F700B0B741 /* RenderingBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673524BD07F700B0B741 /* RenderingBlock.swift */; };
|
||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673724BD086F00B0B741 /* RenderingBlockView.swift */; };
|
||||
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
||||
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */; };
|
||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
||||
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F620258B0811003A0A73 /* BrowserNavigationController.swift */; };
|
||||
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F629258B0833003A0A73 /* InteractivePushTransition.swift */; };
|
||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||
|
@ -298,6 +305,12 @@
|
|||
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>"; };
|
||||
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
|
||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
|
||||
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
||||
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationController.swift; sourceTree = "<group>"; };
|
||||
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -373,6 +386,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */,
|
||||
D62664F024BC0D7700DF9B88 /* GeminiFormat.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -530,6 +544,7 @@
|
|||
D62664EB24BC0B4D00DF9B88 /* DocumentView.swift */,
|
||||
D664673724BD086F00B0B741 /* RenderingBlockView.swift */,
|
||||
D6DA5782252396030048B65A /* View+Extensions.swift */,
|
||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */,
|
||||
);
|
||||
path = GeminiRenderer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -550,11 +565,23 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D688F618258AD231003A0A73 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D688F5FE258ACE6B003A0A73 /* browser.css */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E152A324BFFDF500FDF9D3 /* Gemini-iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */,
|
||||
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */,
|
||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||
|
@ -562,6 +589,7 @@
|
|||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||
D62BCEE1252553620031D894 /* ActivityView.swift */,
|
||||
D688F618258AD231003A0A73 /* Resources */,
|
||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||
D6E152AF24BFFDF600FDF9D3 /* LaunchScreen.storyboard */,
|
||||
D6E152B224BFFDF600FDF9D3 /* Info.plist */,
|
||||
|
@ -748,6 +776,9 @@
|
|||
D68544302522E10F004C4AE0 /* PBXTargetDependency */,
|
||||
);
|
||||
name = GeminiRenderer;
|
||||
packageProductDependencies = (
|
||||
D688F58F258AC814003A0A73 /* HTMLEntities */,
|
||||
);
|
||||
productName = GeminiRenderer;
|
||||
productReference = D62664CE24BC081B00DF9B88 /* GeminiRenderer.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
|
@ -890,6 +921,9 @@
|
|||
Base,
|
||||
);
|
||||
mainGroup = D626645224BBF1C200DF9B88;
|
||||
packageReferences = (
|
||||
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */,
|
||||
);
|
||||
productRefGroup = D626645C24BBF1C200DF9B88 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
@ -968,6 +1002,7 @@
|
|||
files = (
|
||||
D6E152B124BFFDF600FDF9D3 /* LaunchScreen.storyboard in Resources */,
|
||||
D6E152AE24BFFDF600FDF9D3 /* Preview Assets.xcassets in Resources */,
|
||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */,
|
||||
D6E152AB24BFFDF600FDF9D3 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1048,6 +1083,7 @@
|
|||
D62664EE24BC0BCE00DF9B88 /* MaybeLazyVStack.swift in Sources */,
|
||||
D62664EC24BC0B4D00DF9B88 /* DocumentView.swift in Sources */,
|
||||
D6DA5783252396030048B65A /* View+Extensions.swift in Sources */,
|
||||
D688F586258AC738003A0A73 /* GeminiHTMLRenderer.swift in Sources */,
|
||||
D664673824BD086F00B0B741 /* RenderingBlockView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1064,7 +1100,11 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */,
|
||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||
|
@ -1949,6 +1989,25 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Kitura/swift-html-entities";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 3.0.200;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D688F58F258AC814003A0A73 /* HTMLEntities */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D688F58E258AC814003A0A73 /* XCRemoteSwiftPackageReference "swift-html-entities" */;
|
||||
productName = HTMLEntities;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = D626645324BBF1C200DF9B88 /* Project object */;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "HTMLEntities",
|
||||
"repositoryURL": "https://github.com/Kitura/swift-html-entities",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2b14531d0c36dbb7c1c45a4d38db9c2e7898a307",
|
||||
"version": "3.0.200"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -21,6 +21,13 @@ public struct Document {
|
|||
// todo: should this be \r\n
|
||||
return lines.map { $0.geminiText() }.joined(separator: "\n")
|
||||
}
|
||||
|
||||
public var title: String? {
|
||||
for case let .heading(text, level: _) in lines {
|
||||
return text
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension Document {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// GeminiHTMLRenderer.swift
|
||||
// GeminiRenderer
|
||||
//
|
||||
// Created by Shadowfacts on 12/16/20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GeminiFormat
|
||||
import HTMLEntities
|
||||
|
||||
public protocol GeminiHTMLRendererDelegate: class {
|
||||
|
||||
}
|
||||
|
||||
public class GeminiHTMLRenderer {
|
||||
|
||||
public weak var delegate: GeminiHTMLRendererDelegate?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func renderDocumentToHTML(_ doc: Document) -> String {
|
||||
var str = ""
|
||||
|
||||
var inPreformatting = false
|
||||
var inList = false
|
||||
|
||||
for line in doc.lines {
|
||||
if inList && !line.isListItem {
|
||||
str += "</ul>"
|
||||
}
|
||||
|
||||
switch line {
|
||||
case let .text(text):
|
||||
str += "<p>\(text.htmlEscape())</p>"
|
||||
case let .link(url, text: maybeText):
|
||||
let text = maybeText ?? url.absoluteString
|
||||
str += "<p class=\"link\"><a href=\"\(url.absoluteString)\">\(text.htmlEscape())</a></p>"
|
||||
case .preformattedToggle(alt: _):
|
||||
inPreformatting = !inPreformatting
|
||||
if inPreformatting {
|
||||
str += "<pre>"
|
||||
} else {
|
||||
str += "</pre>"
|
||||
}
|
||||
case let .preformattedText(text):
|
||||
str += text.htmlEscape()
|
||||
str += "\n"
|
||||
case let .heading(text, level: level):
|
||||
let tag = "h\(level.rawValue)"
|
||||
str += "<\(tag)>\(text.htmlEscape())</\(tag)>"
|
||||
case let .unorderedListItem(text):
|
||||
if !inList {
|
||||
inList = true
|
||||
str += "<ul>"
|
||||
}
|
||||
str += "<li>\(text.htmlEscape())</li>"
|
||||
case let .quote(text):
|
||||
str += "<blockquote>\(text.htmlEscape())</blockquote>"
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension Document.Line {
|
||||
var isListItem: Bool {
|
||||
switch self {
|
||||
case .unorderedListItem(_):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue