summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.md2
-rwxr-xr-xbrowser.py36
-rw-r--r--fsm.py174
-rw-r--r--gemini.py16
-rw-r--r--style.css29
-rw-r--r--test.gmi49
6 files changed, 295 insertions, 11 deletions
diff --git a/TODO.md b/TODO.md
index 112487c..5788296 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,7 +3,7 @@
- vim-style
- scroll
- highlight and follow links
- - forward and back
+ - forward and bac ak
- url bar
- [ ] search in page
- [ ] work with search pages
diff --git a/browser.py b/browser.py
index 625f4b1..cda35de 100755
--- a/browser.py
+++ b/browser.py
@@ -2,7 +2,7 @@
import sys
-from PySide2 import QtCore, QtWidgets
+from PySide2 import QtCore, QtWidgets, QtGui
import gemini
@@ -28,13 +28,26 @@ class GViewport(QtWidgets.QTextBrowser):
def loadResource(self, type_, url):
gem = gemini.get(url.toString())
+ print(gem['body'])
if 'body' in gem:
html = gemini.gem2html(gem['body'])
else:
html = '<h1>{} {}</h1>'.format(gem['status'], gem['meta'])
- self.resourceLoadedStatus.emit('{status} {meta}'.format(status=gem['status'], meta=gem['meta']))
+ self.resourceLoadedStatus.emit(
+ '{status} {meta}'.format(status=gem['status'], meta=gem['meta'])
+ )
return html
+ def setSource(self, url, type_=None):
+ """
+ send all unsupported urls to the OS
+ """
+ if url.scheme() == 'gemini':
+ if type_ is None:
+ return super().setSource(url)
+ return super().setSource(url, type_)
+ return QtGui.QDesktopServices().openUrl(url)
+
def setRawSource(self):
return self.setSource(QtCore.QUrl(self.address_bar.text()))
@@ -50,12 +63,10 @@ class GBrowser(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
- self.init_ui()
- def init_ui(self):
- request_status = QtWidgets.QLabel()
- self.statusBar().addWidget(request_status)
+ ## Create widgets
+ # Navigation Toolbar
back = QtWidgets.QPushButton("back")
forward = QtWidgets.QPushButton("forward")
address = GUrlBar()
@@ -64,8 +75,17 @@ class GBrowser(QtWidgets.QMainWindow):
toolbar.addWidget(back)
toolbar.addWidget(forward)
toolbar.addWidget(address)
+ self.addToolBar(toolbar)
+
+ # Main Viewport
browser = GViewport(address)
+ self.setCentralWidget(browser)
+ # Status Bar
+ request_status = QtWidgets.QLabel()
+ self.statusBar().addWidget(request_status)
+
+ # Connect signals
back.clicked.connect(browser.backward)
forward.clicked.connect(browser.forward)
browser.sourceChanged.connect(address.setUrl)
@@ -73,8 +93,8 @@ class GBrowser(QtWidgets.QMainWindow):
browser.hoverUrlChanged.connect(self.set_status_url)
browser.resourceLoadedStatus.connect(request_status.setText)
- self.addToolBar(toolbar)
- self.setCentralWidget(browser)
+ #
+ browser.document().setDefaultStyleSheet(open('style.css').read())
browser.setSource(QtCore.QUrl('gemini://gemini.circumlunar.space/'))
diff --git a/fsm.py b/fsm.py
new file mode 100644
index 0000000..40f9c2d
--- /dev/null
+++ b/fsm.py
@@ -0,0 +1,174 @@
+import sys
+
+class StackFSM(object):
+ """
+ Implement a finite state machine that uses a stack to
+ manage state.
+ """
+
+ def __init__(self):
+ self._state_stack = []
+
+ def _current_state(self):
+ if len(self._state_stack) > 0:
+ return self._state_stack[-1]
+ return None
+
+ def update(self):
+ fn = self._current_state()
+ if fn is not None:
+ fn()
+
+ def push_state(self, state):
+ self._state_stack.append(state)
+
+ def pop_state(self):
+ return self._state_stack.pop()
+
+class Parser(object):
+
+ def __init__(self, document, output=None):
+ self._document = document
+ if output is None:
+ output = sys.stdout
+ self._output = output
+ self._offset = 0
+ self._blanks = 0
+ self._fsm = StackFSM()
+
+ def parse(self):
+ self._fsm.push_state(self.text_state)
+ while self._fsm._current_state() is not None:
+ self._fsm.update()
+
+ def text_state(self):
+ if len(self._document) <= self._offset:
+ self._fsm.pop_state()
+ return
+ line = self._document[self._offset]
+ if line.strip() == '':
+ self._blanks += 1
+ else:
+ self._blanks = 0
+ if line.strip() == '```':
+ self._fsm.pop_state()
+ self._fsm.push_state(self.pre_state)
+ self._output.write('<pre>\n')
+ self._offset += 1
+ elif line.startswith('* '):
+ self._fsm.pop_state()
+ self._fsm.push_state(self.list_state)
+ self._output.write('<ul>\n')
+ elif line.startswith('=>'):
+ self._fsm.pop_state()
+ self._fsm.push_state(self.link_state)
+ self._output.write('<ul>\n')
+ else:
+ if line.startswith('# '):
+ self._output.write('<h1>{}</h1>\n'.format(line[2:]))
+ elif line.startswith('## '):
+ self._output.write('<h2>{}</h2>\n'.format(line[3:]))
+ elif line.startswith('### '):
+ self._output.write('<h3>{}</h3>\n'.format(line[4:]))
+ elif line.startswith('> '):
+ self._output.write('<blockquote>{}</blockquote>\n'.format(line[2:]))
+ elif line.strip() == '':
+ if self._blanks > 1:
+ self._output.write('<br/>\n')
+ else:
+ self._output.write('<p>{}</p>\n'.format(line))
+ self._offset += 1
+
+ def pre_state(self):
+ if len(self._document) < self._offset:
+ self.pop_state()
+ return
+ line = self._document[self._offset]
+ if line.strip() == '```':
+ self._fsm.pop_state()
+ self._fsm.push_state(self.text_state)
+ self._output.write('</pre>\n')
+ self._offset += 1
+ elif line.startswith('* '):
+ self._fsm.pop_state()
+ self._fsm.push_state(self.list_state)
+ self._output.write('<ul>\n')
+ elif line.startswith('=>'):
+ self._fsm.pop_state()
+ self._fsm.push_state(self.link_state)
+ self._output.write('<ul>\n')
+ else:
+ self._output.write(line + '\n')
+ self._offset += 1
+
+ def list_state(self):
+ if len(self._document) < self._offset:
+ self.pop_state()
+ return
+ line = self._document[self._offset]
+ if line.startswith('* '):
+ self._output.write('<li>{}</li>\n'.format(line[2:]))
+ self._offset += 1
+ else:
+ self._fsm.pop_state()
+ self._fsm.push_state(self.text_state)
+ self._output.write('</ul>\n')
+
+ def link_state(self):
+ if len(self._document) < self._offset:
+ self.pop_state()
+ return
+ line = self._document[self._offset]
+ if line.startswith('=>'):
+ parts = line[2:].split(None, 2)
+ if len(parts) == 1:
+ self._output.write('<li><a href="{}">{}</a></li>\n'.format(parts[0], parts[0]))
+ else:
+ self._output.write('<li><a href="{}">{}</a></li>\n'.format(parts[0], parts[1]))
+ self._offset += 1
+ else:
+ self._fsm.pop_state()
+ self._fsm.push_state(self.text_state)
+ self._output.write('</ul>\n')
+
+document = """
+# h1
+hello
+hello
+
+## h2
+```
+code
+code
+```
+### h3
+hello
+
+
+hello
+
+### lists
+* hello
+* two
+* three
+
+text
+
+* one
+* two
+* three
+
+### links
+=>https://example.com hello
+=> https://example.com two
+=> https://example.com three
+
+text
+
+=>https://example.com
+=> https://example.com
+=> https://example.com
+"""
+
+p = Parser(document.split('\n'))
+p.parse()
diff --git a/gemini.py b/gemini.py
index 9856449..c3ece46 100644
--- a/gemini.py
+++ b/gemini.py
@@ -8,7 +8,7 @@ def htmlescape(text: str) -> str:
def gem2html(gem: str) -> str:
html = []
- html.append('<style>\n{css}</style>'.format(css=open('style.css').read()))
+ html.append('<html>\n<body>')
state = 'text'
blanklines = 0
for line in gem.split('\n'):
@@ -69,6 +69,7 @@ def gem2html(gem: str) -> str:
html.append('<h1>{}</h1>'.format(line[1:].lstrip()))
else:
html.append('<p>{}</p>'.format(htmlescape(line)))
+ html.append('</body>\n</html>')
return '\n'.join(html)
def urljoin(base: str, url: str) -> str:
@@ -90,6 +91,16 @@ def get(url: str, follow_redirects: bool = True) -> dict:
response = _get(response['meta'])
return response
+def _parse_meta(meta: str) -> dict:
+ mime, _, params_text = meta.lower().strip().partition(';')
+ params = {}
+ if params_text.strip():
+ for param in params_text.split(';'):
+ k, val = param.split('=')
+ params[k.strip()] = val.strip()
+ params['mime'] = mime.strip()
+ return params
+
def _get(url: str) -> dict:
url_parts = urllib.parse.urlsplit(url)
if len(url_parts.path) == 0:
@@ -118,9 +129,10 @@ def _get(url: str) -> dict:
'status': status,
'meta': meta.strip(),
}
+ meta_params = _parse_meta(meta)
body = fp.read()
return {
'status': status,
'meta': meta.strip(),
- 'body': body.decode('utf8'),
+ 'body': body.decode(meta_params.get('charset', 'utf8')),
}
diff --git a/style.css b/style.css
index c4b8372..f2d57ba 100644
--- a/style.css
+++ b/style.css
@@ -1,3 +1,32 @@
+html { font: 100%/1.5 Arial, sans-serif; }
+h1 { font-size: 2em; /* 2*16 = 32 */ }
+h2 { font-size: 1.5em; /* 1.5*16 = 24 */ }
+h3 { font-size: 1.17em; /* 1.17*16 = 18.72 */ }
+h4 { font-size: 1em; /* 1*16 = 16 */ }
+h5 { font-size: 0.83em; /* 0.83*16 = 13.28 */ }
+h6 { font-size: 0.75em; /* 0.75*16 = 12 */ }
+p {
+ line-height: 1.5;
+ margin: 0 0 1em;
+}
+a:active { color: tomato; }
+a:focus { border: 1px dotted tomato; }
+code,
+pre { font-family: monospace, serif;
+font-size: 1em; }
+blockquote {
+ font-style: italic;
+}
+blockquote:before {
+ content: "\201C";
+ display: inline-block;
+ padding-right: .4em;
+}
+
+/*
+ * Begin custom styles.
+ */
+
li.external a {
color: red;
}
diff --git a/test.gmi b/test.gmi
new file mode 100644
index 0000000..aca420f
--- /dev/null
+++ b/test.gmi
@@ -0,0 +1,49 @@
+# Header Level 1
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+Quisque eget ultricies arcu, vel sollicitudin risus.
+
+## Header Level 2
+
+Donec dictum est metus, id placerat nibh tempor id.
+
+Donec quis massa commodo, commodo orci elementum, mollis est.
+
+### Header Level 3
+
+Ut ac finibus nisl. Aliquam aliquam felis eu dolor tempor, ut ultricies urna tempus.
+
+## Text
+
+Two blank lines follow.
+
+
+Three blank lines follow.
+
+
+
+That's it!
+
+## Lists
+* Mercury
+* Gemini
+* Apollo
+
+## Links
+
+=> https://example.com A cool website
+=> gopher://example.com An even cooler gopherhole
+=> gemini://example.com A supremely cool Gemini capsule
+=> sftp://example.com
+
+## Blockquotes
+
+> Gemtext supports blockquotes. The quoted content is written as a single long line, which begins with a single > character
+
+## Preformatted text
+
+```
+Pre- formatted text
+should be aligned
+```