First commit
This commit is contained in:
parent
930149a383
commit
0c6136348c
12
README.md
12
README.md
|
@ -1,2 +1,12 @@
|
|||
# wobbly
|
||||
# Wobbly, the Web Browser Based Gemini Browser
|
||||
|
||||
This is a very basic browser for the gemini network protocol, implemented as a web service.
|
||||
|
||||
The frontend is a webpage designed to look like a simple browser window. It has some javascript to make the experience smoother and reduce load on the backend. It also has a startpage in geminispace.
|
||||
|
||||
The backend is a CGI script that takes a URL and fetches it over gemini, parsing and transforming it into an HTML response which is then presented in the browser's main window.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The backend depends on `gemcall` and `gemtextparser`, available near this repository.
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Welcome to Wobbly!
|
||||
|
||||
You might be wondering what this is. That's okay.
|
||||
|
||||
You know how website addresses start with http:// or https://? Those are network protocols, and they're not the only ones. The gemini network protocol was invented in 2019. It has some serious drawbacks that makes it a bad replacement for HTTP/HTTPS, but that's also its main selling point. Its real aim is a simplified experience, both as a developer, producer and consumer of content.
|
||||
|
||||
=> gemini://gemini.circumlunar.space/ You can read more about gemini here.
|
||||
|
||||
But a web browser doesn't understand the gemini network protocol, nor the gemtext markup language. It speaks HTTP/HTTPS and HTML/CSS/JavaScript. Therefore, I've made Wobbly.
|
||||
|
||||
Wobbly is a gemini browser that runs inside your web browser. It's very basic:
|
||||
* It has a help button.
|
||||
* Back, Up (in the directory tree), Forward.
|
||||
* A URL address bar, and a button to Go!
|
||||
|
||||
Try opening this web page in full screen mode in your web browser! It'll probably look more familiar then.
|
||||
|
||||
Wanna see what's up in geminispace?
|
||||
|
||||
=> gemini://warmedal.se/~antenna/ Check out Antenna, which aggregates a lot of content published in the last week.
|
||||
|
||||
Cheers,
|
||||
ew0k
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3
|
||||
# vim: tabstop=4 shiftwidth=4 expandtab
|
||||
|
||||
import gemcall
|
||||
import gemtextparser
|
||||
import urllib.parse
|
||||
import sys
|
||||
from os import getenv
|
||||
|
||||
urllib.parse.uses_relative.append("gemini")
|
||||
urllib.parse.uses_netloc.append("gemini")
|
||||
|
||||
def clean(text):
|
||||
return text.replace('<','<').replace('>','>')
|
||||
|
||||
print("Content-type: text/html\n\n")
|
||||
|
||||
if getenv('QUERY_STRING'):
|
||||
try:
|
||||
baseurl = getenv('QUERY_STRING').split('?')[0]
|
||||
response = gemcall.request(baseurl)
|
||||
if response.responsecode in [30, 31]:
|
||||
newtarget = urllib.parse.urljoin(baseurl, response.meta)
|
||||
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {response.meta}\"</h1><p>This address redirects to <a href='{newtarget}'>{newtarget}</a>.</p>")
|
||||
elif response.responsecode == 20 and "text/" in response.meta:
|
||||
responsetext = response.read(20*1024) # We don't support more than 20kb
|
||||
if "text/gemini" in response.meta:
|
||||
parser = gemtextparser.GemtextParser()
|
||||
preform = False
|
||||
listmode = False
|
||||
blockquote = False
|
||||
for line in parser.parseText(responsetext.decode()):
|
||||
if line.linetype == gemtextparser.LineType.PREFORM:
|
||||
if not preform:
|
||||
print("<pre><code>")
|
||||
preform = True
|
||||
print(clean(line.text()))
|
||||
continue
|
||||
if not line.linetype == gemtextparser.LineType.PREFORM and preform:
|
||||
print("</code></pre>")
|
||||
preform = False
|
||||
|
||||
if line.linetype == gemtextparser.LineType.LISTITEM:
|
||||
if not listmode:
|
||||
print("<ul>")
|
||||
listmode = True
|
||||
print(f"<li>{clean(line.text())}</li>")
|
||||
continue
|
||||
if not line.linetype == gemtextparser.LineType.LISTITEM and listmode:
|
||||
print("</ul>")
|
||||
listmode = False
|
||||
|
||||
if line.linetype == gemtextparser.LineType.BLOCKQUOTE:
|
||||
if not blockquote:
|
||||
print("<blockquote>")
|
||||
blockquote = True
|
||||
print(clean(line.text()))
|
||||
continue
|
||||
if not line.linetype == gemtextparser.LineType.BLOCKQUOTE and blockquote:
|
||||
print("</blockquote>")
|
||||
blockquote = False
|
||||
|
||||
if line.linetype == gemtextparser.LineType.H1 and line.text():
|
||||
print(f"<h1>{clean(line.text())}</h1>")
|
||||
continue
|
||||
if line.linetype == gemtextparser.LineType.H2 and line.text():
|
||||
print(f"<h2>{clean(line.text())}</h2>")
|
||||
continue
|
||||
if line.linetype == gemtextparser.LineType.H3 and line.text():
|
||||
print(f"<h3>{clean(line.text())}</h3>")
|
||||
continue
|
||||
|
||||
if line.linetype == gemtextparser.LineType.LINK:
|
||||
linkURL = urllib.parse.urljoin(baseurl, clean(line.linkURL().replace("'","")))
|
||||
target = "" if linkURL.startswith("gemini://") else " target='_blank'"
|
||||
if line.text():
|
||||
print(f"<a href='{linkURL}'{target}>{clean(line.text())}</a><br/>")
|
||||
else:
|
||||
print(f"<a href='{linkURL}'{target}>{linkURL}</a><br/>")
|
||||
continue
|
||||
|
||||
if line.linetype == gemtextparser.LineType.PLAIN:
|
||||
print(f"{clean(line.text())}<br/>")
|
||||
if listmode:
|
||||
print("</ul>")
|
||||
if blockquote:
|
||||
print("</blockquote>")
|
||||
if preform:
|
||||
print("</code></pre>")
|
||||
|
||||
else:
|
||||
print("<pre><code>")
|
||||
print(clean(responsetext.decode()))
|
||||
print("</code></pre>")
|
||||
|
||||
elif response.responsecode == 20:
|
||||
print(f"<h1>RESPONSE: \"20 Success\", but...</h1><p>The Wobbly browser only supports text responses, and this response is '{clean(response.meta)}'.</p>")
|
||||
elif response.responsecode in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59]:
|
||||
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>This is an error message from the server. Hopefully it makes sense to you.</p>")
|
||||
elif response.responsecode in [60, 61, 62]:
|
||||
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>The Wobbly browser does not support client certificates. Please install a more full-featured browser to visit this page.</p>")
|
||||
elif response.responsecode in [10,11]:
|
||||
print(f"<h1>RESPONSE: \"{str(response.responsecode)} {clean(response.meta)}\"</h1><p>The status codes 10 and 11 are requests for user input.</p><p>The only means of submitting user input to a server in gemini is via the so-called QUERY_STRING; the part of a URL that follows after a '?'. The Wobbly browser does not support this for privacy/security reasons. Please install a more full-featured browser to be able to submit data.</p>")
|
||||
|
||||
response.discard()
|
||||
except:
|
||||
print(f"<h1>Unknown Error</h1><p>Something went wrong when trying to fetch '{getenv('QUERY_STRING')}'. Could not recover.</p>")
|
||||
else:
|
||||
print("<h1>What?</h1><p>I'm sorry, but I don't know what you want me to do without a URL.</p>")
|
|
@ -0,0 +1,134 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Wobbly, the Web-based Gemini Browser</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel='icon' href='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>♊️</text></svg>'>
|
||||
<style>
|
||||
body { margin: 0; padding: 0 0 5em}
|
||||
#wobbly-toolbar { background-color: #cccccc; width: 100%; position: fixed; top: 0; }
|
||||
#wobbly-tools { display: flex; max-width: 60em; padding: 0.2em; margin: auto; }
|
||||
#wobbly-tools input { flex-basis: 100%; }
|
||||
#wobbly-help { display: none; background-color: #ffffcc; }
|
||||
#wobbly-help-text { max-width: 45em; margin: auto; padding: 1.5em 1em 0.5em; }
|
||||
#wobbly-window { max-width: 45em; margin: auto; padding: 2em 1em 5em; }
|
||||
</style>
|
||||
<script>
|
||||
var geminiHistory = []
|
||||
var geminiPresent = 0
|
||||
function geminiInit(){
|
||||
document.getElementById("wobbly-window").innerHTML = ""
|
||||
var startpage = "gemini://warmedal.se/~wobbly/"
|
||||
document.getElementById("wobbly-addressbar").value = startpage
|
||||
geminiHistory.push({url:startpage})
|
||||
geminiGo()
|
||||
}
|
||||
function geminiHelpToggle(){
|
||||
if (document.getElementById("wobbly-help-button").innerHTML === 'Help') {
|
||||
document.getElementById("wobbly-help").style.display = 'block'
|
||||
document.getElementById("wobbly-help-button").innerHTML = "Unhelp"
|
||||
} else {
|
||||
document.getElementById("wobbly-help").style.display = 'none'
|
||||
document.getElementById("wobbly-help-button").innerHTML = "Help"
|
||||
}
|
||||
}
|
||||
function geminiGo(){
|
||||
url = document.getElementById("wobbly-addressbar").value.trim().split("?")[0]
|
||||
if (url.length == 0) {
|
||||
return
|
||||
}
|
||||
if (! url.startsWith("gemini://")) {
|
||||
url = "gemini://" + url
|
||||
}
|
||||
if (! (url === geminiHistory[geminiPresent].url)) {
|
||||
geminiPresent++
|
||||
geminiHistory[geminiPresent] = {url:url}
|
||||
while (geminiHistory.length > geminiPresent +1) {
|
||||
geminiHistory.pop()
|
||||
}
|
||||
}
|
||||
var xmlHttp = new XMLHttpRequest()
|
||||
xmlHttp.open( "GET", "browser.cgi?" + url )
|
||||
xmlHttp.onload = function (e) {
|
||||
var body = xmlHttp.responseText
|
||||
document.getElementById("wobbly-window").innerHTML = body
|
||||
geminiHistory[geminiPresent].body = body
|
||||
}
|
||||
xmlHttp.send(null)
|
||||
}
|
||||
function geminiBack(){
|
||||
if (geminiPresent > 0) {
|
||||
geminiPresent--
|
||||
document.getElementById("wobbly-addressbar").value = geminiHistory[geminiPresent].url
|
||||
document.getElementById("wobbly-window").innerHTML = geminiHistory[geminiPresent].body
|
||||
}
|
||||
}
|
||||
// Big thanks to idiomdrottning for fixing my geminiUp function <3
|
||||
function geminiUp(){
|
||||
var components = geminiHistory[geminiPresent].url.match(/\/[^/]*/g)
|
||||
if ("/" == components.pop()) components.pop()
|
||||
if (components.length == 1) return
|
||||
document.getElementById("wobbly-addressbar").value = "gemini:" + components.join("") + "/"
|
||||
geminiGo()
|
||||
}
|
||||
function geminiForward(){
|
||||
if (geminiPresent < geminiHistory.length - 1) {
|
||||
geminiPresent++
|
||||
document.getElementById("wobbly-addressbar").value = geminiHistory[geminiPresent].url
|
||||
document.getElementById("wobbly-window").innerHTML = geminiHistory[geminiPresent].body
|
||||
}
|
||||
}
|
||||
function geminiEnterListener(e) {
|
||||
if(e.keyCode === 13) {
|
||||
e.preventDefault()
|
||||
geminiGo()
|
||||
}
|
||||
}
|
||||
function geminiLinkClickListener(e) {
|
||||
var e = window.e || e;
|
||||
if (e.target.tagName === 'A' && e.target.href.startsWith("gemini://")) {
|
||||
// We only care about clicked gemini:// links
|
||||
e.preventDefault()
|
||||
document.getElementById("wobbly-addressbar").value = e.target.href
|
||||
geminiGo()
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('click', geminiLinkClickListener, false);
|
||||
} else {
|
||||
document.attachEvent('onclick', geminiLinkClickListener);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="geminiInit()">
|
||||
<div id="wobbly-toolbar">
|
||||
<div id="wobbly-tools">
|
||||
<button onclick="geminiHelpToggle()" id="wobbly-help-button">Help</button>
|
||||
<button onclick="geminiBack()" id="wobbly-back">⬅️</button>
|
||||
<button onclick="geminiUp()" id="wobbly-up">⬆️</button>
|
||||
<button onclick="geminiForward()" id="wobbly-forward">➡️</button>
|
||||
<input id="wobbly-addressbar" onkeypress="geminiEnterListener(event)"/>
|
||||
<button onclick="geminiGo()" id="wobbly-go">Go!</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wobbly-help">
|
||||
<div id="wobbly-help-text">
|
||||
<h1>Wobbly Help Section</h1>
|
||||
<ul>
|
||||
<li>This browser is for casual reading. It only supports text documents, and only cares about any sort of layout in gemtext ones.</li>
|
||||
<li>Javascript has no notion of sockets that aren't http/https. Therefore all traffic from here goes through a web server that fetches and serves the gemini pages translated to html. Input and client certificates are not supported, because they would be unsafe under these circumstances.</li>
|
||||
<li>'Back' and 'Forward' buttons take you to cached versions of those pages. Pressing 'Go!' without typing in a new URL is a refresh.</li>
|
||||
<li>'Up' means to go one level higher in the path on the current domain. If you are on 'gemini://warmedal.se/~antenna/' and press 'Up' you will visit 'gemini://warmedal.se/'.</li>
|
||||
<li>Non-gemini links will open in a new tab in your browser as usual!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wobbly-window">
|
||||
<h1>This Browser Requires Javascript</h1>
|
||||
<p>Sorry, there's no meaningful way around that. It's not terribly much, however. Feel free to check the source code.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue