Improving lists and making an abstract renderer
This commit is contained in:
parent
b85e9e8f04
commit
3029a3a218
218
offpunk.py
218
offpunk.py
|
@ -214,6 +214,7 @@ def fix_ipv6_url(url):
|
||||||
return schema + "://" + schemaless
|
return schema + "://" + schemaless
|
||||||
return schemaless
|
return schemaless
|
||||||
|
|
||||||
|
# This list is also used as a list of supported protocols
|
||||||
standard_ports = {
|
standard_ports = {
|
||||||
"gemini": 1965,
|
"gemini": 1965,
|
||||||
"gopher": 70,
|
"gopher": 70,
|
||||||
|
@ -221,26 +222,35 @@ standard_ports = {
|
||||||
"https" : 443,
|
"https" : 443,
|
||||||
}
|
}
|
||||||
|
|
||||||
# First, we define the gemtext and html renderers, outside of the rest
|
# First, we define the different content->text renderers, outside of the rest
|
||||||
# (They could later be factorized in other files or replaced)
|
# (They could later be factorized in other files or replaced)
|
||||||
|
class AbstractRenderer():
|
||||||
# Gemtext Rendering Engine
|
|
||||||
# this method takes the original gemtext and returns
|
|
||||||
# [rendered_text,links_table]
|
|
||||||
class GemtextRenderer():
|
|
||||||
def __init__(self,content,url):
|
def __init__(self,content,url):
|
||||||
self.url = url
|
self.url = url
|
||||||
self.body = content
|
self.body = content
|
||||||
self.rendered_text = None
|
self.rendered_text = None
|
||||||
self.links = None
|
self.links = None
|
||||||
self.title = None
|
self.title = None
|
||||||
|
self.validity = True
|
||||||
|
|
||||||
|
def with_width(func):
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if not kwargs["width"]:
|
||||||
|
kwargs["width"] = TERM_WIDTH
|
||||||
|
func(*args, **kwargs)
|
||||||
|
return inner
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
return True
|
return self.validity
|
||||||
|
|
||||||
|
@with_width
|
||||||
def get_body(self,readable=True,width=None):
|
def get_body(self,readable=True,width=None):
|
||||||
if not width:
|
pass
|
||||||
width = TERM_WIDTH
|
|
||||||
|
# Gemtext Rendering Engine
|
||||||
|
class GemtextRenderer(AbstractRenderer):
|
||||||
|
def get_body(self,readable=True,width=None):
|
||||||
|
super().get_body(readable=readable,width=width)
|
||||||
if self.rendered_text == None :
|
if self.rendered_text == None :
|
||||||
self.rendered_text, self.links = self.render_gemtext(self.body,width=width)
|
self.rendered_text, self.links = self.render_gemtext(self.body,width=width)
|
||||||
return self.rendered_text
|
return self.rendered_text
|
||||||
|
@ -343,21 +353,9 @@ class GemtextRenderer():
|
||||||
rendered_text += wrap_line(line).rstrip() + "\n"
|
rendered_text += wrap_line(line).rstrip() + "\n"
|
||||||
return rendered_text, links
|
return rendered_text, links
|
||||||
|
|
||||||
class GopherRenderer():
|
class GopherRenderer(AbstractRenderer):
|
||||||
def __init__(self,content,url):
|
|
||||||
self.url = url
|
|
||||||
self.body = content
|
|
||||||
self.rendered_text = None
|
|
||||||
self.links = None
|
|
||||||
self.title = None
|
|
||||||
self.validity = True
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_body(self,readable=True,width=None):
|
def get_body(self,readable=True,width=None):
|
||||||
if not width:
|
super().get_body(readable=readable,width=width)
|
||||||
width = TERM_WIDTH
|
|
||||||
if self.rendered_text == None:
|
if self.rendered_text == None:
|
||||||
self.rendered_text,self.links = self.menu_or_text(width=width)
|
self.rendered_text,self.links = self.menu_or_text(width=width)
|
||||||
return self.rendered_text
|
return self.rendered_text
|
||||||
|
@ -423,22 +421,13 @@ class GopherRenderer():
|
||||||
return rendered_text,links
|
return rendered_text,links
|
||||||
|
|
||||||
|
|
||||||
class FeedRenderer():
|
class FeedRenderer(AbstractRenderer):
|
||||||
def __init__(self,content,url):
|
|
||||||
self.url = url
|
|
||||||
self.body = content
|
|
||||||
self.rendered_text = None
|
|
||||||
self.links = None
|
|
||||||
self.title = None
|
|
||||||
self.validity = True
|
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
self.render_feed(self.body)
|
self.render_feed(self.body)
|
||||||
return self.validity
|
return self.validity
|
||||||
|
|
||||||
def get_body(self,readable=True,width=None):
|
def get_body(self,readable=True,width=None):
|
||||||
if not width:
|
super().get_body(readable=readable,width=width)
|
||||||
width = TERM_WIDTH
|
|
||||||
if readable:
|
if readable:
|
||||||
if not self.rendered_text:
|
if not self.rendered_text:
|
||||||
self.rendered_text = self.render_feed(self.body,width=width)
|
self.rendered_text = self.render_feed(self.body,width=width)
|
||||||
|
@ -509,27 +498,21 @@ class FeedRenderer():
|
||||||
page += "\n\n"
|
page += "\n\n"
|
||||||
return page
|
return page
|
||||||
|
|
||||||
class ImageRenderer():
|
class ImageRenderer(AbstractRenderer):
|
||||||
def __init__(self,img,url):
|
|
||||||
self.url = url
|
|
||||||
self.path = img
|
|
||||||
self.rendered_text = None
|
|
||||||
self.title = "Picture file"
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
if _RENDER_IMAGE:
|
if _RENDER_IMAGE:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
def get_body(self,readable=True,width=None):
|
def get_body(self,readable=True,width=None):
|
||||||
if not width:
|
super().get_body(readable=readable,width=width)
|
||||||
width = TERM_WIDTH
|
|
||||||
if self.rendered_text == None:
|
if self.rendered_text == None:
|
||||||
self.rendered_text = self.render_image(self.path,width)
|
self.rendered_text = self.render_image(self.body,width)
|
||||||
return self.rendered_text
|
return self.rendered_text
|
||||||
def get_links(self):
|
def get_links(self):
|
||||||
return []
|
return []
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.title
|
return "Picture file"
|
||||||
def render_image(self,img,width=None):
|
def render_image(self,img,width=None):
|
||||||
if not width:
|
if not width:
|
||||||
width = TERM_WIDTH
|
width = TERM_WIDTH
|
||||||
|
@ -545,20 +528,9 @@ class ImageRenderer():
|
||||||
ansi_img = "***image failed : %s***\n" %err
|
ansi_img = "***image failed : %s***\n" %err
|
||||||
return ansi_img
|
return ansi_img
|
||||||
|
|
||||||
class HtmlRenderer():
|
class HtmlRenderer(AbstractRenderer):
|
||||||
def __init__(self,content,url):
|
|
||||||
self.url = url
|
|
||||||
self.body = content
|
|
||||||
self.rendered_text = None
|
|
||||||
self.links = None
|
|
||||||
self.title = None
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_body(self,readable=True,width=None):
|
def get_body(self,readable=True,width=None):
|
||||||
if not width:
|
super().get_body(readable=readable,width=width)
|
||||||
width = TERM_WIDTH
|
|
||||||
if self.rendered_text == None or not readable:
|
if self.rendered_text == None or not readable:
|
||||||
if readable:
|
if readable:
|
||||||
mode = "readable"
|
mode = "readable"
|
||||||
|
@ -675,7 +647,7 @@ class HtmlRenderer():
|
||||||
src = element.get("src")
|
src = element.get("src")
|
||||||
text = ""
|
text = ""
|
||||||
ansi_img = ""
|
ansi_img = ""
|
||||||
if _RENDER_IMAGE and mode != "quick":
|
if _RENDER_IMAGE and mode != "quick" and src:
|
||||||
abs_url = urllib.parse.urljoin(self.url, src)
|
abs_url = urllib.parse.urljoin(self.url, src)
|
||||||
g = GeminiItem(abs_url)
|
g = GeminiItem(abs_url)
|
||||||
if g.is_cache_valid():
|
if g.is_cache_valid():
|
||||||
|
@ -700,15 +672,10 @@ class HtmlRenderer():
|
||||||
elif element.name == "br":
|
elif element.name == "br":
|
||||||
rendered_body = "\n"
|
rendered_body = "\n"
|
||||||
elif element.string:
|
elif element.string:
|
||||||
#print("tag without children:",element.name)
|
|
||||||
#print("string : **%s** "%element.string.strip())
|
|
||||||
#print("########")
|
|
||||||
rendered_body = sanitize_string(element.string)
|
rendered_body = sanitize_string(element.string)
|
||||||
else:
|
else:
|
||||||
#print("tag children:",element.name)
|
|
||||||
for child in element.children:
|
for child in element.children:
|
||||||
rendered_body += recursive_render(child,indent=indent)
|
rendered_body += recursive_render(child,indent=indent)
|
||||||
#print("body for element %s: %s"%(element.name,rendered_body))
|
|
||||||
return indent + rendered_body
|
return indent + rendered_body
|
||||||
# the real render_html hearth
|
# the real render_html hearth
|
||||||
if mode == "full":
|
if mode == "full":
|
||||||
|
@ -716,7 +683,6 @@ class HtmlRenderer():
|
||||||
else:
|
else:
|
||||||
readable = Document(body)
|
readable = Document(body)
|
||||||
summary = readable.summary()
|
summary = readable.summary()
|
||||||
#r_body += "\x1b[1;34m\x1b[4m" + self.get_title() + "\x1b[0m""\n"
|
|
||||||
soup = BeautifulSoup(summary, 'html.parser')
|
soup = BeautifulSoup(summary, 'html.parser')
|
||||||
rendered_body = ""
|
rendered_body = ""
|
||||||
if soup and soup.body :
|
if soup and soup.body :
|
||||||
|
@ -742,9 +708,6 @@ class HtmlRenderer():
|
||||||
subsequent_indent=s_indent)
|
subsequent_indent=s_indent)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
wrapped = line
|
wrapped = line
|
||||||
#print(self.url)
|
|
||||||
#print(err)
|
|
||||||
#crash
|
|
||||||
wrapped += "\n"
|
wrapped += "\n"
|
||||||
else:
|
else:
|
||||||
wrapped = ""
|
wrapped = ""
|
||||||
|
@ -755,14 +718,14 @@ class HtmlRenderer():
|
||||||
first_line = ""
|
first_line = ""
|
||||||
while first_line == "" and len(lines) > 0:
|
while first_line == "" and len(lines) > 0:
|
||||||
first_line = lines.pop(0)
|
first_line = lines.pop(0)
|
||||||
if self.get_title()[:79] not in first_line:
|
if self.get_title()[:(width-1)] not in first_line:
|
||||||
title = "\x1b[1;34m\x1b[4m" + self.get_title() + "\x1b[0m""\n"
|
title = "\x1b[1;34m\x1b[4m" + self.get_title() + "\x1b[0m""\n"
|
||||||
title = textwrap.fill(title,width)
|
title = textwrap.fill(title,width)
|
||||||
r_body = title + "\n" + r_body
|
r_body = title + "\n" + r_body
|
||||||
return r_body,links
|
return r_body,links
|
||||||
|
|
||||||
# Mapping mimetypes with renderers
|
# Mapping mimetypes with renderers
|
||||||
# (any content with a mimetype text/* not listed here will be rendered with render_gemtext)
|
# (any content with a mimetype text/* not listed here will be rendered with as GemText)
|
||||||
_FORMAT_RENDERERS = {
|
_FORMAT_RENDERERS = {
|
||||||
"text/gemini": GemtextRenderer,
|
"text/gemini": GemtextRenderer,
|
||||||
"text/html" : HtmlRenderer,
|
"text/html" : HtmlRenderer,
|
||||||
|
@ -789,6 +752,7 @@ class GeminiItem():
|
||||||
self.mime = None
|
self.mime = None
|
||||||
self.renderer = None
|
self.renderer = None
|
||||||
self.links = None
|
self.links = None
|
||||||
|
self.body = None
|
||||||
parsed = urllib.parse.urlparse(self.url)
|
parsed = urllib.parse.urlparse(self.url)
|
||||||
if "./" in url or url[0] == "/":
|
if "./" in url or url[0] == "/":
|
||||||
self.scheme = "file"
|
self.scheme = "file"
|
||||||
|
@ -927,6 +891,8 @@ class GeminiItem():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_body(self,as_file=False):
|
def get_body(self,as_file=False):
|
||||||
|
if self.body and not as_file:
|
||||||
|
return self.body
|
||||||
if self.local:
|
if self.local:
|
||||||
path = self.path
|
path = self.path
|
||||||
elif self.is_cache_valid():
|
elif self.is_cache_valid():
|
||||||
|
@ -1055,33 +1021,35 @@ class GeminiItem():
|
||||||
filename = os.path.basename(self.get_cache_path())
|
filename = os.path.basename(self.get_cache_path())
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
def write_body(self,body,mime):
|
def write_body(self,body,mime=None):
|
||||||
## body is a copy of the raw gemtext
|
## body is a copy of the raw gemtext
|
||||||
## Write_body() also create the cache !
|
## Write_body() also create the cache !
|
||||||
# DEFAULT GEMINI MIME
|
# DEFAULT GEMINI MIME
|
||||||
|
self.body = body
|
||||||
if mime:
|
if mime:
|
||||||
self.mime, mime_options = cgi.parse_header(mime)
|
self.mime, mime_options = cgi.parse_header(mime)
|
||||||
if self.mime and self.mime.startswith("text/"):
|
if not self.local:
|
||||||
mode = "w"
|
if self.mime and self.mime.startswith("text/"):
|
||||||
else:
|
mode = "w"
|
||||||
mode = "wb"
|
else:
|
||||||
cache_dir = os.path.dirname(self._cache_path)
|
mode = "wb"
|
||||||
# If the subdirectory already exists as a file (not a folder)
|
cache_dir = os.path.dirname(self._cache_path)
|
||||||
# We remove it (happens when accessing URL/subfolder before
|
# If the subdirectory already exists as a file (not a folder)
|
||||||
# URL/subfolder/file.gmi.
|
# We remove it (happens when accessing URL/subfolder before
|
||||||
# This causes loss of data in the cache
|
# URL/subfolder/file.gmi.
|
||||||
# proper solution would be to save "sufolder" as "sufolder/index.gmi"
|
# This causes loss of data in the cache
|
||||||
# If the subdirectory doesn’t exist, we recursively try to find one
|
# proper solution would be to save "sufolder" as "sufolder/index.gmi"
|
||||||
# until it exists to avoid a file blocking the creation of folders
|
# If the subdirectory doesn’t exist, we recursively try to find one
|
||||||
root_dir = cache_dir
|
# until it exists to avoid a file blocking the creation of folders
|
||||||
while not os.path.exists(root_dir):
|
root_dir = cache_dir
|
||||||
root_dir = os.path.dirname(root_dir)
|
while not os.path.exists(root_dir):
|
||||||
if os.path.isfile(root_dir):
|
root_dir = os.path.dirname(root_dir)
|
||||||
os.remove(root_dir)
|
if os.path.isfile(root_dir):
|
||||||
os.makedirs(cache_dir,exist_ok=True)
|
os.remove(root_dir)
|
||||||
with open(self._cache_path, mode=mode) as f:
|
os.makedirs(cache_dir,exist_ok=True)
|
||||||
f.write(body)
|
with open(self._cache_path, mode=mode) as f:
|
||||||
f.close()
|
f.write(body)
|
||||||
|
f.close()
|
||||||
|
|
||||||
def get_mime(self):
|
def get_mime(self):
|
||||||
if self.mime:
|
if self.mime:
|
||||||
|
@ -1363,7 +1331,7 @@ class GeminiClient(cmd.Cmd):
|
||||||
self.gi = gi
|
self.gi = gi
|
||||||
return
|
return
|
||||||
# check if local file exists.
|
# check if local file exists.
|
||||||
if gi.local and not os.path.isfile(gi.path):
|
if gi.local and not os.path.exists(gi.path):
|
||||||
print("Local file %s does not exist!" %gi.path)
|
print("Local file %s does not exist!" %gi.path)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1428,15 +1396,9 @@ class GeminiClient(cmd.Cmd):
|
||||||
self.page_index = 0
|
self.page_index = 0
|
||||||
self.index_index = -1
|
self.index_index = -1
|
||||||
if display:
|
if display:
|
||||||
# We actually put the body in a tmpfile before giving it to less
|
self._temp_file(rendered_body)
|
||||||
if self.idx_filename:
|
|
||||||
os.unlink(self.idx_filename)
|
|
||||||
tmpf = tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False)
|
|
||||||
tmpf.write(rendered_body)
|
|
||||||
tmpf.close()
|
|
||||||
self.idx_filename = tmpf.name
|
|
||||||
cmd_str = _DEFAULT_CAT
|
cmd_str = _DEFAULT_CAT
|
||||||
subprocess.run(shlex.split(cmd_str % tmpf.name))
|
subprocess.run(shlex.split(cmd_str % self.idx_filename))
|
||||||
elif display :
|
elif display :
|
||||||
cmd_str = self._get_handler_cmd(gi.get_mime())
|
cmd_str = self._get_handler_cmd(gi.get_mime())
|
||||||
try:
|
try:
|
||||||
|
@ -1446,13 +1408,23 @@ class GeminiClient(cmd.Cmd):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Handler program %s not found!" % shlex.split(cmd_str)[0])
|
print("Handler program %s not found!" % shlex.split(cmd_str)[0])
|
||||||
print("You can use the ! command to specify another handler program or pipeline.")
|
print("You can use the ! command to specify another handler program or pipeline.")
|
||||||
|
|
||||||
# Update state
|
# Update state
|
||||||
self.gi = gi
|
self.gi = gi
|
||||||
if update_hist and not self.sync_only:
|
if update_hist and not self.sync_only:
|
||||||
self._update_history(gi)
|
self._update_history(gi)
|
||||||
|
|
||||||
|
|
||||||
|
def _temp_file(self,content):
|
||||||
|
# We actually put the body in a tmpfile before giving it to less
|
||||||
|
if self.idx_filename:
|
||||||
|
os.unlink(self.idx_filename)
|
||||||
|
tmpf = tempfile.NamedTemporaryFile("w", encoding="UTF-8", delete=False)
|
||||||
|
tmpf.write(content)
|
||||||
|
tmpf.close()
|
||||||
|
self.idx_filename = tmpf.name
|
||||||
|
return self.idx_filename
|
||||||
|
|
||||||
|
|
||||||
def _fetch_http(self,gi):
|
def _fetch_http(self,gi):
|
||||||
header = {}
|
header = {}
|
||||||
header["User-Agent"] = "Offpunk browser v%s"%_VERSION
|
header["User-Agent"] = "Offpunk browser v%s"%_VERSION
|
||||||
|
@ -1976,8 +1948,8 @@ class GeminiClient(cmd.Cmd):
|
||||||
if self.sync_only:
|
if self.sync_only:
|
||||||
return
|
return
|
||||||
# We don’t add lists to history
|
# We don’t add lists to history
|
||||||
if not gi or os.path.join(_DATA_DIR,"lists") in gi.url:
|
#if not gi or os.path.join(_DATA_DIR,"lists") in gi.url:
|
||||||
return
|
# return
|
||||||
histlist = self.get_list("history")
|
histlist = self.get_list("history")
|
||||||
links = self.list_get_links("history")
|
links = self.list_get_links("history")
|
||||||
# avoid duplicate
|
# avoid duplicate
|
||||||
|
@ -2874,18 +2846,42 @@ See also :
|
||||||
- archive (to remove current page from all lists while adding to archives)"""
|
- archive (to remove current page from all lists while adding to archives)"""
|
||||||
listdir = os.path.join(_DATA_DIR,"lists")
|
listdir = os.path.join(_DATA_DIR,"lists")
|
||||||
os.makedirs(listdir,exist_ok=True)
|
os.makedirs(listdir,exist_ok=True)
|
||||||
|
def write_list(l):
|
||||||
|
path = os.path.join(listdir,l+".gmi")
|
||||||
|
size = len(self.list_get_links(l))
|
||||||
|
line = "=> %s %s (%s items)\n" %(str(path),l,size)
|
||||||
|
return line
|
||||||
|
|
||||||
if not arg:
|
if not arg:
|
||||||
lists = self.list_lists()
|
lists = self.list_lists()
|
||||||
if len(lists) > 0:
|
if len(lists) > 0:
|
||||||
self.lookup = []
|
body = ""
|
||||||
|
my_lists = []
|
||||||
|
system_lists = []
|
||||||
|
subscriptions = []
|
||||||
lists.sort()
|
lists.sort()
|
||||||
for l in lists:
|
for l in lists:
|
||||||
gpath = os.path.join(listdir,l+".gmi")
|
if l in ["history","to_fetch","archives","tour"]:
|
||||||
size = len(self.list_get_links(l))
|
system_lists.append(l)
|
||||||
gi = GeminiItem(gpath,l+" (%s items)"%size)
|
elif l in ["subscribed"]:
|
||||||
self.lookup.append(gi)
|
subscriptions.append(l)
|
||||||
self._show_lookup(url=False)
|
else:
|
||||||
self.page_index = 0
|
my_lists.append(l)
|
||||||
|
if len(my_lists) > 0:
|
||||||
|
body+= "\n## Bookmarks Lists\n"
|
||||||
|
for l in my_lists:
|
||||||
|
body += write_list(l)
|
||||||
|
if len(subscriptions) > 0:
|
||||||
|
body +="\n## Subscriptions\n"
|
||||||
|
for l in subscriptions:
|
||||||
|
body += write_list(l)
|
||||||
|
if len(system_lists) > 0:
|
||||||
|
body +="\n## System Lists\n"
|
||||||
|
for l in system_lists:
|
||||||
|
body += write_list(l)
|
||||||
|
lgi = GeminiItem(listdir, "My lists")
|
||||||
|
lgi.write_body(body,"text/gemini")
|
||||||
|
self._go_to_gi(lgi)
|
||||||
else:
|
else:
|
||||||
print("No lists yet. Use `list create`")
|
print("No lists yet. Use `list create`")
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Reference in New Issue