@@ 72,6 72,10 @@ def qute_fifo() -> str:
return os.environ["QUTE_FIFO"]
+def html_href(url: str, description: str) -> str:
+ return "".join(['<a href="', url, '">', description, "</a>"])
+
+
def qute_gemini_css_path() -> str:
"""Return the path where the custom CSS file is expected to be."""
try:
@@ 81,17 85,17 @@ def qute_gemini_css_path() -> str:
return os.path.join(base_dir, "qutebrowser/userscripts/qute-gemini.css")
-def gemini_absolutise_url(base: str, relative: str) -> str:
+def gemini_absolutise_url(base_url: str, relative_url: str) -> str:
"""Absolutise relative gemini URLs.
Adapted from gcat: https://github.com/aaronjanse/gcat
"""
- if "://" not in relative:
+ if "://" not in relative_url:
# Python's URL tools somehow only work with known schemes?
- base = base.replace("gemini://", "http://")
- relative = urllib.parse.urljoin(base, relative)
- relative = relative.replace("http://", "gemini://")
- return relative
+ base_url = base_url.replace("gemini://", "http://")
+ relative_url = urllib.parse.urljoin(base_url, relative_url)
+ relative_url = relative_url.replace("http://", "gemini://")
+ return relative_url
def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]:
@@ 103,6 107,7 @@ def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]:
Adapted from gcat: https://github.com/aaronjanse/gcat
"""
+ # Parse the URL to get the hostname and port
parsed_url = urllib.parse.urlparse(url)
if not parsed_url.scheme:
url = "gemini://" + url
@@ 113,20 118,21 @@ def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]:
useport = parsed_url.port
else:
useport = 1965
- # Do the Gemini transaction
+ # Do the Gemini transaction, looping for redirects
redirects = 0
while True:
+ # Send the request
s = socket.create_connection((parsed_url.hostname, useport))
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
s = context.wrap_socket(s, server_hostname = parsed_url.netloc)
s.sendall((url + "\r\n").encode("UTF-8"))
- # Get header and check for redirects
+ # Get the status code and meta
fp = s.makefile("rb")
header = fp.readline().decode("UTF-8").strip()
status, meta = header.split()[:2]
- # Follow redirects
+ # Follow up to 5 redirects
if status.startswith("3"):
url = gemini_absolutise_url(url, meta)
parsed_url = urllib.parse.urlparse(url)
@@ 142,18 148,17 @@ def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]:
error_msg = ""
# 2x Success
if status.startswith("2"):
- # Decode according to declared charset
media_type, media_type_opts = cgi.parse_header(meta)
+ # Decode according to declared charset defaulting to UTF-8
if meta.startswith("text/gemini"):
- content = fp.read()
- content = content.decode(media_type_opts.get("charset", "UTF-8"))
+ charset = media_type_opts.get("charset", "UTF-8")
+ content = fp.read().decode(charset)
else:
error_msg = "Expected media type text/gemini but received " \
+ media_type
# Handle errors
else:
- # Try matching a 2-digit status code before trying to match a 1-digit
- # one
+ # Try matching a 2-digit and then a 1-digit status code
try:
error_msg = _status_code_desc[status[0:2]]
except KeyError:
@@ 161,6 166,7 @@ def gemini_fetch_url(url: str) -> Tuple[str, str, str, str, str]:
error_msg = _status_code_desc[status[0]]
except KeyError:
error_msg = "The server sent back something weird."
+ # Substitute the contents of meta into the error message if needed
error_msg = error_msg.replace("META", meta)
return content, url, status, meta, error_msg
@@ 170,23 176,24 @@ def gemtext_to_html(gemtext: str, url: str, original_url: str,
"""Convert gemtext to HTML.
title: Used as the document title.
- url: The URL the gemtext was received from. Used to resolve relative
- URLs in the gemtext content.
- status: The Gemini status code returned by the server.
- meta: The meta returned by the server.
+ url: The URL the gemtext was received from. Used to resolve
+ relative URLs in the gemtext content.
+ original_url: The URL the original request was made at.
+ status: The Gemini status code returned by the server.
+ meta: The meta returned by the server.
Returns the HTML representation as a string.
"""
# Accumulate converted gemtext lines
lines = ['<?xml version="1.0" encoding="UTF-8"?>',
- '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">',
- "\t<head>",
- "\t\t<title>" + html.escape(url) + "</title>",
- "\t\t<style>",
- get_css(),
- "\t\t</style>",
- "\t</head>",
- "\t<body>",
- "\t<article>"]
+ '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">',
+ "\t<head>",
+ "\t\t<title>" + html.escape(url) + "</title>",
+ "\t\t<style>",
+ get_css(),
+ "\t\t</style>",
+ "\t</head>",
+ "\t<body>",
+ "\t<article>"]
in_pre = False
in_list = False
# Add an extra newline to ensure list tags are closed properly
@@ 195,9 202,8 @@ def gemtext_to_html(gemtext: str, url: str, original_url: str,
if not line.startswith("*") and in_list:
lines.append("\t\t</ul>")
in_list = False
- # Blank line
+ # Blank line, ignore
if not line:
- # Ignore
pass
# Link
elif line.startswith("=>"):
@@ 209,7 215,7 @@ def gemtext_to_html(gemtext: str, url: str, original_url: str,
l[1] = html.escape(l[1])
# Resolve relative URLs
l[0] = gemini_absolutise_url(url, l[0])
- lines.append("".join(['\t\t<p><a href="', l[0], '">', l[1], "</a></p>"]))
+ lines.append("\t\t<p>" + html_href(l[0], l[1]) + "</p>")
# Preformated toggle
elif line.startswith("```"):
if in_pre:
@@ 235,31 241,33 @@ def gemtext_to_html(gemtext: str, url: str, original_url: str,
lines.append("\t\t\t<li>" + html.escape(line[1:].strip()) + "</li>")
# Quote
elif line.startswith(">"):
- lines.append("\t\t<blockquote>\n\t\t\t<p>" + line[1:].strip() + "</p>\n\t\t</blockquote>")
+ lines.extend(["\t\t<blockquote>",
+ "\t\t\t<p>" + line[1:].strip() + "</p>",
+ "\t\t</blockquote>"])
# Normal text
else:
lines.append("\t\t<p>" + html.escape(line.strip()) + "</p>")
- lines.append("")
- url_html = '<a href="' + url + '">' + html.escape(url) + "</a>"
- original_url_html = '<a href="' + original_url + '">' + html.escape(url) + "</a>"
- lines.append("\t</article>")
- lines.append("\t<details>")
- lines.append("\t\t<summary>")
- lines.append("\t\t\tContent from " + url_html)
- lines.append("\t\t</summary>")
- lines.append("\t\t<dl>")
- lines.append("\t\t\t<dt>Original URL</dt>")
- lines.append("\t\t\t<dd>" + original_url_html + "</dd>")
- lines.append("\t\t\t<dt>Status</dt>")
- lines.append("\t\t\t<dd>" + status + "</dd>")
- lines.append("\t\t\t<dt>Meta</dt>")
- lines.append("\t\t\t<dd>" + meta + "</dd>")
- lines.append("\t\t\t<dt>Fetched by</dt>")
- lines.append('\t\t\t<dd><a href="https://git.sr.ht/~sotirisp/qute-gemini">qute-gemini ' + str(_version) + "</a></dd>")
- lines.append("\t\t</dl>")
- lines.append("\t</details>")
- lines.append("\t</body>\n")
- lines.append("</html>")
+ url_html = html_href(url, html.escape(url))
+ original_url_html = html_href(original_url, html.escape(original_url))
+ lines.extend(["",
+ "\t</article>",
+ "\t<details>",
+ "\t\t<summary>",
+ "\t\t\tContent from " + url_html,
+ "\t\t</summary>",
+ "\t\t<dl>",
+ "\t\t\t<dt>Original URL</dt>",
+ "\t\t\t<dd>" + original_url_html + "</dd>",
+ "\t\t\t<dt>Status</dt>",
+ "\t\t\t<dd>" + status + "</dd>",
+ "\t\t\t<dt>Meta</dt>",
+ "\t\t\t<dd>" + meta + "</dd>",
+ "\t\t\t<dt>Fetched by</dt>",
+ '\t\t\t<dd><a href="https://git.sr.ht/~sotirisp/qute-gemini">qute-gemini ' + str(_version) + "</a></dd>",
+ "\t\t</dl>",
+ "\t</details>",
+ "\t</body>",
+ "</html>"])
return "\n".join(lines)
@@ 296,16 304,16 @@ def open_gemini(url: str, open_args: str) -> None:
# Get the Gemini content
content, content_url, status, meta, error_msg = gemini_fetch_url(url)
if error_msg:
+ # Generate an error page in a data URI
open_url = qute_error_page(url, error_msg)
else:
- # Convert to HTML in a temporary file
+ # Success, convert to HTML in a temporary file
tmpf = tempfile.NamedTemporaryFile("w", suffix=".html", delete=False)
tmp_filename = tmpf.name
tmpf.close()
with open(tmp_filename, "w") as f:
f.write(gemtext_to_html(content, content_url, url, status, meta))
open_url = " file://" + tmp_filename
-
# Open the HTML file in qutebrowser
with open(qute_fifo(), "w") as qfifo:
qfifo.write("open " + open_args + open_url)
@@ 323,7 331,6 @@ if __name__ == "__main__":
open_args = "-t"
else:
open_args = ""
-
# Select how to open the URL depending on its scheme
url = qute_url()
parsed_url = urllib.parse.urlparse(url)