mime: basic MIME type management
This commit is contained in:
parent
2767402d9f
commit
dd4a4196a8
|
@ -131,10 +131,10 @@ class Browser:
|
|||
elif char == ord("l"):
|
||||
self.scroll_page_horizontally(1)
|
||||
self.screen.nodelay(False)
|
||||
|
||||
ctrl_char = curses.unctrl(char)
|
||||
if ctrl_char == "a":
|
||||
self.set_status("yup!")
|
||||
# else:
|
||||
# unctrled = curses.unctrl(char)
|
||||
# if unctrled == b"^T":
|
||||
# self.set_status("test!")
|
||||
|
||||
@property
|
||||
def page_pad_size(self):
|
||||
|
@ -281,13 +281,14 @@ class Browser:
|
|||
return
|
||||
|
||||
if response.code == 20:
|
||||
# TODO handle MIME type; assume it's gemtext for now.
|
||||
text = response.content.decode("utf-8", errors="replace")
|
||||
self.load_page(Page.from_gemtext(text))
|
||||
if self.current_url and history:
|
||||
self.history.push(self.current_url)
|
||||
self.current_url = url
|
||||
self.set_status(url)
|
||||
handle_code = self.handle_response_content(response)
|
||||
if handle_code == 0:
|
||||
if self.current_url and history:
|
||||
self.history.push(self.current_url)
|
||||
self.current_url = url
|
||||
self.set_status(url)
|
||||
elif handle_code == 1:
|
||||
self.set_status(f"Downloaded {url}.")
|
||||
elif response.generic_code == 30 and response.meta:
|
||||
self.open_url(response.meta, base_url=url, redirects=redirects + 1)
|
||||
elif response.generic_code in (40, 50):
|
||||
|
@ -299,6 +300,41 @@ class Browser:
|
|||
error = f"Unhandled response code {response.code}"
|
||||
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):
|
||||
"""Load Gemtext data as the current page."""
|
||||
old_pad_height = self.page_pad.dim[0]
|
||||
|
|
|
@ -47,7 +47,8 @@ class CommandLine:
|
|||
command = ""
|
||||
except TerminateCommandInterrupt as exc:
|
||||
command = exc.command
|
||||
command = command[1:].rstrip()
|
||||
else:
|
||||
command = command[1:].rstrip()
|
||||
curses.curs_set(0)
|
||||
self.clear()
|
||||
return command
|
||||
|
|
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
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
|
||||
|
||||
|
@ -190,11 +192,16 @@ class Response:
|
|||
MAX_META_LEN = 1024
|
||||
|
||||
@property
|
||||
def generic_code(self):
|
||||
def generic_code(self) -> int:
|
||||
"""See `Response.get_generic_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
|
||||
def parse(data):
|
||||
def parse(data: bytes) -> Optional["Response"]:
|
||||
"""Parse a received response."""
|
||||
try:
|
||||
response_header_len = data.index(LINE_TERM)
|
||||
|
@ -216,6 +223,6 @@ class Response:
|
|||
return response
|
||||
|
||||
@staticmethod
|
||||
def get_generic_code(code):
|
||||
def get_generic_code(code) -> int:
|
||||
"""Return the generic version (x0) of this code."""
|
||||
return code - (code % 10)
|
||||
|
|
Reference in a new issue