mime: basic MIME type management
This commit is contained in:
parent
2767402d9f
commit
dd4a4196a8
|
@ -131,10 +131,10 @@ class Browser:
|
||||||
elif char == ord("l"):
|
elif char == ord("l"):
|
||||||
self.scroll_page_horizontally(1)
|
self.scroll_page_horizontally(1)
|
||||||
self.screen.nodelay(False)
|
self.screen.nodelay(False)
|
||||||
|
# else:
|
||||||
ctrl_char = curses.unctrl(char)
|
# unctrled = curses.unctrl(char)
|
||||||
if ctrl_char == "a":
|
# if unctrled == b"^T":
|
||||||
self.set_status("yup!")
|
# self.set_status("test!")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def page_pad_size(self):
|
def page_pad_size(self):
|
||||||
|
@ -281,13 +281,14 @@ class Browser:
|
||||||
return
|
return
|
||||||
|
|
||||||
if response.code == 20:
|
if response.code == 20:
|
||||||
# TODO handle MIME type; assume it's gemtext for now.
|
handle_code = self.handle_response_content(response)
|
||||||
text = response.content.decode("utf-8", errors="replace")
|
if handle_code == 0:
|
||||||
self.load_page(Page.from_gemtext(text))
|
|
||||||
if self.current_url and history:
|
if self.current_url and history:
|
||||||
self.history.push(self.current_url)
|
self.history.push(self.current_url)
|
||||||
self.current_url = url
|
self.current_url = url
|
||||||
self.set_status(url)
|
self.set_status(url)
|
||||||
|
elif handle_code == 1:
|
||||||
|
self.set_status(f"Downloaded {url}.")
|
||||||
elif response.generic_code == 30 and response.meta:
|
elif response.generic_code == 30 and response.meta:
|
||||||
self.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
self.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
||||||
elif response.generic_code in (40, 50):
|
elif response.generic_code in (40, 50):
|
||||||
|
@ -299,6 +300,41 @@ class Browser:
|
||||||
error = f"Unhandled response code {response.code}"
|
error = f"Unhandled response code {response.code}"
|
||||||
self.set_status_error(error)
|
self.set_status_error(error)
|
||||||
|
|
||||||
|
def handle_response_content(self, response: Response) -> int:
|
||||||
|
"""Handle a response's content from a Gemini server.
|
||||||
|
|
||||||
|
According to the MIME type received or inferred, render or download the
|
||||||
|
response's content.
|
||||||
|
|
||||||
|
Currently only text/gemini content is rendered.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- response: a successful Response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An error code: 0 means a page has been loaded, so any book-keeping such
|
||||||
|
as history management can be applied; 1 means a content has been
|
||||||
|
successfully retrieved but has not been displayed (e.g. non-text
|
||||||
|
content) nor saved as a page; 2 means that the content could not be
|
||||||
|
handled, either due to bogus MIME type or MIME parameters.
|
||||||
|
"""
|
||||||
|
mime_type = response.get_mime_type()
|
||||||
|
if mime_type.main_type == "text":
|
||||||
|
if mime_type.sub_type == "gemini":
|
||||||
|
encoding = mime_type.charset
|
||||||
|
try:
|
||||||
|
text = response.content.decode(encoding, errors="replace")
|
||||||
|
except LookupError:
|
||||||
|
self.set_status_error("Unknown encoding {encoding}.")
|
||||||
|
return 2
|
||||||
|
self.load_page(Page.from_gemtext(text))
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
pass # TODO
|
||||||
|
else:
|
||||||
|
pass # TODO
|
||||||
|
return 1
|
||||||
|
|
||||||
def load_page(self, page: Page):
|
def load_page(self, page: Page):
|
||||||
"""Load Gemtext data as the current page."""
|
"""Load Gemtext data as the current page."""
|
||||||
old_pad_height = self.page_pad.dim[0]
|
old_pad_height = self.page_pad.dim[0]
|
||||||
|
|
|
@ -47,6 +47,7 @@ class CommandLine:
|
||||||
command = ""
|
command = ""
|
||||||
except TerminateCommandInterrupt as exc:
|
except TerminateCommandInterrupt as exc:
|
||||||
command = exc.command
|
command = exc.command
|
||||||
|
else:
|
||||||
command = command[1:].rstrip()
|
command = command[1:].rstrip()
|
||||||
curses.curs_set(0)
|
curses.curs_set(0)
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
40
bebop/mime.py
Normal file
40
bebop/mime.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""Basic MIME utilities (RFC 2046)."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CHARSET = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MimeType:
|
||||||
|
main_type: str
|
||||||
|
sub_type: str
|
||||||
|
parameters: dict
|
||||||
|
|
||||||
|
@property
|
||||||
|
def charset(self):
|
||||||
|
return self.parameters.get("charset", DEFAULT_CHARSET)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_str(mime_string) -> Optional["MimeType"]:
|
||||||
|
"""Parse a MIME string into a MimeType instance, or None on error."""
|
||||||
|
if ";" in mime_string:
|
||||||
|
type_str, *parameters = mime_string.split(";")
|
||||||
|
parameters = {}
|
||||||
|
for param in map(lambda s: s.strip().lower(), parameters):
|
||||||
|
if param.count("=") != 1:
|
||||||
|
return None
|
||||||
|
param_name, param_value = param.split("=")
|
||||||
|
parameters[param_name] = param_value
|
||||||
|
else:
|
||||||
|
type_str = mime_string.strip()
|
||||||
|
parameters = {}
|
||||||
|
if type_str.count("/") != 1:
|
||||||
|
return None
|
||||||
|
main_type, sub_type = type_str.split("/")
|
||||||
|
return MimeType(main_type, sub_type, parameters)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_MIME_TYPE = MimeType("text", "gemini", {"charset": DEFAULT_CHARSET})
|
|
@ -5,7 +5,9 @@ import socket
|
||||||
import ssl
|
import ssl
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from bebop.mime import DEFAULT_MIME_TYPE, MimeType
|
||||||
from bebop.tofu import CertStatus, CERT_STATUS_INVALID, validate_cert
|
from bebop.tofu import CertStatus, CERT_STATUS_INVALID, validate_cert
|
||||||
|
|
||||||
|
|
||||||
|
@ -190,11 +192,16 @@ class Response:
|
||||||
MAX_META_LEN = 1024
|
MAX_META_LEN = 1024
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def generic_code(self):
|
def generic_code(self) -> int:
|
||||||
|
"""See `Response.get_generic_code`."""
|
||||||
return Response.get_generic_code(self.code)
|
return Response.get_generic_code(self.code)
|
||||||
|
|
||||||
|
def get_mime_type(self) -> MimeType:
|
||||||
|
"""Return the MIME type if possible, else the default MIME type."""
|
||||||
|
return MimeType.from_str(self.meta) or DEFAULT_MIME_TYPE
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(data):
|
def parse(data: bytes) -> Optional["Response"]:
|
||||||
"""Parse a received response."""
|
"""Parse a received response."""
|
||||||
try:
|
try:
|
||||||
response_header_len = data.index(LINE_TERM)
|
response_header_len = data.index(LINE_TERM)
|
||||||
|
@ -216,6 +223,6 @@ class Response:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_generic_code(code):
|
def get_generic_code(code) -> int:
|
||||||
"""Return the generic version (x0) of this code."""
|
"""Return the generic version (x0) of this code."""
|
||||||
return code - (code % 10)
|
return code - (code % 10)
|
||||||
|
|
Reference in a new issue