diff options
Diffstat (limited to 'browser.py')
-rw-r--r-- | browser.py | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/browser.py b/browser.py new file mode 100644 index 0000000..9d1d421 --- /dev/null +++ b/browser.py @@ -0,0 +1,220 @@ +import sys +import socket +import ssl + +from PySide2 import QtCore, QtWidgets + +def htmlescape(text): + return text.replace('<', '<').replace('>', '>') + +def gem2html(gem): + html = [] + state = 'text' + blanklines = 0 + for line in gem.split('\n'): + if line.startswith('```'): + if state == 'pre': + newstate = 'text' + blanklines = 0 + else: + newstate = 'pre' + elif state == 'pre': + newstate = 'pre' + elif line.startswith('=>'): + newstate = 'links' + elif line.startswith('* '): + newstate = 'list' + else: + newstate = 'text' + + if state != 'pre': + if len(line.strip()) == 0: + blanklines += 1 + if blanklines > 1: + html.append('<br/>') + continue + blanklines = 0 + + if state != newstate: + if state in ('links', 'list'): + html.append('</ul>') + elif state == 'pre': + html.append('</pre>') + + if newstate in ('links', 'list'): + html.append('<ul>') + elif newstate == 'pre': + html.append('<pre>') + state = newstate + + if line.startswith('```'): + pass + elif state == 'links': + tokens = line.split(None, 2) + if len(tokens) == 3: + _, url, text = tokens + html.append('<li><a href="{url}">{text}</a></li>'.format(url=url, text=text)) + else: + _, url = tokens + html.append('<li><a href="{url}">{url}</a></li>'.format(url=url)) + elif state == 'list': + html.append('<li>{}</li>'.format(line[2:])) + elif state == 'pre': + html.append(line) + else: + if line.startswith('###'): + html.append('<h3>{}</h3>'.format(line[3:].lstrip())) + elif line.startswith('##'): + html.append('<h2>{}</h2>'.format(line[2:].lstrip())) + elif line.startswith('#'): + html.append('<h1>{}</h1>'.format(line[1:].lstrip())) + else: + html.append('<p>{}</p>'.format(htmlescape(line))) + return '\n'.join(html) + +def absolute_url(base, url): + """ + modifies `url` in place + """ + if not url.scheme(): + url.setScheme(base.scheme()) + if not url.host(): + url.setHost(base.host()) + if not url.path().startswith('/'): + url.setPath(base.path().rsplit('/', 1)[0] + '/' + url.path()) + if url.port() == -1: + url.setPort(1965) + return url + +def gem_get(url): + if len(url.path()) == 0: + url.setPath('/') + return { + 'status': '32', + 'meta': url.toDisplayString(), + } + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket.create_connection((url.host(), url.port())) as sock: + with context.wrap_socket(sock, server_hostname=url.host()) as ssock: + ssock.sendall('gemini://{}{}\r\n'.format(url.host(), url.path()).encode('utf8')) + fp = ssock.makefile(mode='rb') + header = fp.readline(1027) + status, meta = header.decode('utf8').split(None, 1) + if status[0] != '2': + return { + 'status': status, + 'meta': meta.strip(), + } + body = fp.read() + return { + 'status': status, + 'meta': meta.strip(), + 'body': body.decode('utf8'), +} + +class GViewport(QtWidgets.QTextBrowser): + + hoverUrlChanged = QtCore.Signal(str) + + def __init__(self, address_bar): + self._current_url = None + self._last_redirect = (QtCore.QUrl(), {}) + self.address_bar = address_bar + self._hover_url = None + QtWidgets.QTextBrowser.__init__(self) + + def mouseMoveEvent(self, event): + cur = self.cursorForPosition(event.localPos().toPoint()) + hover_url = cur.charFormat().anchorHref() + hover_url = absolute_url(self._current_url, QtCore.QUrl(hover_url)) + if hover_url != self._hover_url: + print(hover_url) + self._hover_url = hover_url + self.hoverUrlChanged.emit(self._hover_url.toString()) + return super().mouseMoveEvent(event) + + def loadResource(self, type_, url): + if self._last_redirect[0].toString() == url.toString(): + gem = self._last_redirect[1] + else: + if not url.scheme(): + url.setScheme(self._current_url.scheme()) + if not url.host(): + url.setHost(self._current_url.host()) + if url.port() == -1: + url.setPort(1965) + if not url.path().startswith('/'): + url.setPath(self._current_url.path().rsplit('/', 1)[0] + '/' + url.path()) + gem = gem_get(absolute_url(self._current_url, url)) + if 'body' in gem: + html = gem2html(gem['body']) + else: + html = '<h1>{} {}</h1>'.format(gem['status'], gem['meta']) + self._current_url = url + return html + + def setSource(self, url): + if url.scheme() != 'gemini': + return + gem = gem_get(absolute_url(self._current_url, url)) + while gem['status'][0] == '3': + url = QtCore.QUrl(gem['meta']) + if url.port() == 1965: + url.setPort(-1) + print('redirect: {}'.format(url)) + gem = gem_get(absolute_url(self._current_url, url)) + self._last_redirect = (url, gem) + if url.port() == 1965: + url.setPort(-1) + print('setSource: {}'.format(url)) + return super().setSource(url) + + def setRawSource(self): + return self.setSource(QtCore.QUrl(self.address_bar.text())) + +class GUrlBar(QtWidgets.QLineEdit): + + def __init__(self): + QtWidgets.QLineEdit.__init__(self) + + def setUrl(self, url): + return self.setText(url.toDisplayString()) + +class GBrowser(QtWidgets.QMainWindow): + + def __init__(self): + super(GBrowser, self).__init__() + self.initUI() + + def initUI(self): + self.statusBar().showMessage('Ready') + + back = QtWidgets.QPushButton("back") + forward = QtWidgets.QPushButton("forward") + address = GUrlBar() + toolbar = QtWidgets.QToolBar() + toolbar.addWidget(back) + toolbar.addWidget(forward) + toolbar.addWidget(address) + browser = GViewport(address) + + back.clicked.connect(browser.backward) + forward.clicked.connect(browser.forward) + browser.sourceChanged.connect(address.setUrl) + address.returnPressed.connect(browser.setRawSource) + browser.hoverUrlChanged.connect(self.statusBar().showMessage) + + self.addToolBar(toolbar) + self.setCentralWidget(browser) + + browser.setSource(QtCore.QUrl('gemini://gemini.circumlunar.space/')) + + self.setGeometry(10, 10, 1024, 750) + self.setWindowTitle('Gemini Browser') + self.show() + +app = QtWidgets.QApplication(sys.argv) +ex = GBrowser() +sys.exit(app.exec_()) |