page: rework rendering params into RenderOptions
Also make the list item bullet configurable, if you don't like the •.
This commit is contained in:
parent
8d82c1bd53
commit
7ed83f9389
|
@ -18,7 +18,6 @@ from bebop.bookmarks import (
|
|||
save_bookmark,
|
||||
)
|
||||
from bebop.colors import A_ITALIC, ColorPair, init_colors
|
||||
from bebop.config import RENDER_MODES
|
||||
from bebop.command_line import CommandLine
|
||||
from bebop.external import open_external_program
|
||||
from bebop.fs import get_capsule_prefs_path, get_identities_list_path
|
||||
|
@ -26,7 +25,7 @@ from bebop.help import get_help
|
|||
from bebop.history import History
|
||||
from bebop.identity import load_identities
|
||||
from bebop.links import Links
|
||||
from bebop.metalines import LineType
|
||||
from bebop.metalines import LineType, RENDER_MODES
|
||||
from bebop.mime import MimeType
|
||||
from bebop.mouse import ButtonState
|
||||
from bebop.navigation import (
|
||||
|
@ -37,7 +36,7 @@ from bebop.navigation import (
|
|||
parse_url,
|
||||
unparse_url,
|
||||
)
|
||||
from bebop.page import Page
|
||||
from bebop.page import Page, get_render_options
|
||||
from bebop.page_pad import PagePad
|
||||
from bebop.preferences import load_capsule_prefs, save_capsule_prefs
|
||||
from bebop.welcome import WELCOME_PAGE
|
||||
|
@ -671,7 +670,7 @@ class Browser:
|
|||
|
||||
def open_internal_page(self, name, gemtext):
|
||||
"""Open some content corresponding to a "bebop:" internal URL."""
|
||||
page = Page.from_gemtext(gemtext, self.config["text_width"])
|
||||
page = Page.from_gemtext(gemtext, get_render_options(self.config))
|
||||
self.load_page(page)
|
||||
self.current_url = "bebop:" + name
|
||||
|
||||
|
@ -813,18 +812,17 @@ class Browser:
|
|||
def toggle_render_mode(self):
|
||||
"""Switch to the next render mode for the current page."""
|
||||
page = self.current_page
|
||||
if not page or page.render is None:
|
||||
if not page or page.render_opts is None:
|
||||
return
|
||||
if page.render not in RENDER_MODES:
|
||||
render_opts = page.render_opts
|
||||
current_mode = render_opts.mode
|
||||
if current_mode not in RENDER_MODES:
|
||||
next_mode = RENDER_MODES[0]
|
||||
else:
|
||||
cur_mod_index = RENDER_MODES.index(page.render)
|
||||
cur_mod_index = RENDER_MODES.index(current_mode)
|
||||
next_mode = RENDER_MODES[(cur_mod_index + 1) % len(RENDER_MODES)]
|
||||
new_page = Page.from_gemtext(
|
||||
page.source,
|
||||
wrap_at=self.config["text_width"],
|
||||
render=next_mode
|
||||
)
|
||||
render_opts.mode = next_mode
|
||||
new_page = Page.from_gemtext(page.source, render_opts)
|
||||
self.load_page(new_page)
|
||||
self.set_status(f"Using render mode '{next_mode}'.")
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
from urllib.parse import quote, unquote
|
||||
|
||||
from bebop.browser.browser import Browser
|
||||
from bebop.page import Page
|
||||
from bebop.page import Page, get_render_options
|
||||
|
||||
|
||||
def open_file(browser: Browser, filepath: str, encoding="utf-8"):
|
||||
|
@ -36,7 +36,7 @@ def open_file(browser: Browser, filepath: str, encoding="utf-8"):
|
|||
browser.set_status_error(f"Failed to open file: {exc}")
|
||||
return None
|
||||
if path.suffix == ".gmi":
|
||||
page = Page.from_gemtext(text, browser.config["text_width"])
|
||||
page = Page.from_gemtext(text, get_render_options(browser.config))
|
||||
else:
|
||||
page = Page.from_text(text)
|
||||
browser.load_page(page)
|
||||
|
@ -48,8 +48,8 @@ def open_file(browser: Browser, filepath: str, encoding="utf-8"):
|
|||
if entry.is_dir():
|
||||
name += "/"
|
||||
gemtext += f"=> {entry_path} {name}\n"
|
||||
wrap_at = browser.config["text_width"]
|
||||
browser.load_page(Page.from_gemtext(gemtext, wrap_at))
|
||||
render_opts = get_render_options(browser.config)
|
||||
browser.load_page(Page.from_gemtext(gemtext, render_opts))
|
||||
file_url = f"file://{path}"
|
||||
browser.current_url = file_url
|
||||
return file_url
|
||||
|
|
|
@ -12,7 +12,7 @@ from bebop.identity import (
|
|||
get_identities_for_url, load_identities, save_identities
|
||||
)
|
||||
from bebop.navigation import set_parameter
|
||||
from bebop.page import Page
|
||||
from bebop.page import Page, get_render_options
|
||||
from bebop.preferences import get_url_render_mode_pref
|
||||
from bebop.protocol import Request, Response
|
||||
from bebop.tofu import trust_fingerprint, untrust_fingerprint, WRONG_FP_ALERT
|
||||
|
@ -138,7 +138,7 @@ def _handle_untrusted_cert(browser: Browser, request: Request):
|
|||
)
|
||||
alert_page = Page.from_gemtext(
|
||||
alert_page_source,
|
||||
browser.config["text_width"]
|
||||
get_render_options(browser.config)
|
||||
)
|
||||
browser.load_page(alert_page)
|
||||
|
||||
|
@ -214,13 +214,11 @@ def _handle_successful_response(browser: Browser, response: Response, url: str):
|
|||
except LookupError:
|
||||
error = f"Unknown encoding {encoding}."
|
||||
else:
|
||||
text_width = browser.config["text_width"]
|
||||
render_mode = get_url_render_mode_pref(
|
||||
browser.capsule_prefs,
|
||||
url,
|
||||
browser.config["render_mode"]
|
||||
)
|
||||
page = Page.from_gemtext(text, text_width, render=render_mode)
|
||||
render_opts = get_render_options(browser.config)
|
||||
pref_mode = get_url_render_mode_pref(browser.capsule_prefs, url)
|
||||
if pref_mode:
|
||||
render_opts.mode = pref_mode
|
||||
page = Page.from_gemtext(text, render_opts)
|
||||
else:
|
||||
encoding = "utf-8"
|
||||
text = response.content.decode(encoding, errors="replace")
|
||||
|
|
|
@ -32,10 +32,9 @@ DEFAULT_CONFIG = {
|
|||
"scroll_step": 3,
|
||||
"persistent_history": False,
|
||||
"enabled_plugins": [],
|
||||
"list_item_bullet": "• ",
|
||||
}
|
||||
|
||||
RENDER_MODES = ("fancy", "dumb")
|
||||
|
||||
|
||||
def load_config(config_path: Path):
|
||||
if not config_path.is_file():
|
||||
|
|
|
@ -72,6 +72,7 @@ Here are the available options:
|
|||
* generate_client_cert_command (see note 3): command to generate a client cert.
|
||||
* history_limit (int): maximum entries in history.
|
||||
* home (string): home page.
|
||||
* list_item_bullet (string): text shown before every list item.
|
||||
* persistent_history (bool): save and reload history.
|
||||
* render_mode (string): default render mode to use ("fancy" or "dumb").
|
||||
* scroll_step (int): number of lines/columns to scroll in one step.
|
||||
|
|
|
@ -9,6 +9,7 @@ elements classes as they are quite coupled to Gemtext parsing/rendering.
|
|||
"""
|
||||
|
||||
import string
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
|
@ -18,7 +19,6 @@ from bebop.gemtext import (
|
|||
|
||||
SPLIT_CHARS = " \t-"
|
||||
JOIN_CHAR = "-"
|
||||
LIST_ITEM_MARK = "• "
|
||||
|
||||
|
||||
class LineType(IntEnum):
|
||||
|
@ -39,7 +39,18 @@ class LineType(IntEnum):
|
|||
ERROR = 9 # Not part of Gemtext but useful internally.
|
||||
|
||||
|
||||
def generate_metalines(elements, width, dumb=False):
|
||||
RENDER_MODES = ("fancy", "dumb")
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderOptions:
|
||||
"""Rendering options."""
|
||||
width: int
|
||||
mode: str
|
||||
bullet: str
|
||||
|
||||
|
||||
def generate_metalines(elements: list, options: RenderOptions) -> list:
|
||||
"""Format elements into a list of lines with metadata.
|
||||
|
||||
The returned list ("metalines") are tuples (meta, line), meta being a
|
||||
|
@ -54,11 +65,9 @@ def generate_metalines(elements, width, dumb=False):
|
|||
|
||||
Arguments:
|
||||
- elements: list of elements to use.
|
||||
- width: max text width to use.
|
||||
- dumb: if True, standard presentation margins are ignored.
|
||||
- options: RenderOptions to respect when generating metalines.
|
||||
"""
|
||||
metalines = []
|
||||
context = {"width": width}
|
||||
separator = ({"type": LineType.NONE}, "")
|
||||
has_margins = False
|
||||
thin_type = None
|
||||
|
@ -68,28 +77,28 @@ def generate_metalines(elements, width, dumb=False):
|
|||
has_margins = False
|
||||
thin_type = None
|
||||
if isinstance(element, Title):
|
||||
element_metalines = format_title(element, context)
|
||||
element_metalines = format_title(element, options)
|
||||
has_margins = True
|
||||
elif isinstance(element, Paragraph):
|
||||
element_metalines = format_paragraph(element, context)
|
||||
element_metalines = format_paragraph(element, options)
|
||||
has_margins = True
|
||||
elif isinstance(element, Link):
|
||||
element_metalines = format_link(element, context)
|
||||
element_metalines = format_link(element, options)
|
||||
thin_type = LineType.LINK
|
||||
elif isinstance(element, Preformatted):
|
||||
element_metalines = format_preformatted(element, context)
|
||||
element_metalines = format_preformatted(element, options)
|
||||
has_margins = True
|
||||
elif isinstance(element, Blockquote):
|
||||
element_metalines = format_blockquote(element, context)
|
||||
element_metalines = format_blockquote(element, options)
|
||||
has_margins = True
|
||||
elif isinstance(element, ListItem):
|
||||
element_metalines = format_list_item(element, context)
|
||||
element_metalines = format_list_item(element, options)
|
||||
thin_type = LineType.LIST_ITEM
|
||||
else:
|
||||
continue
|
||||
# In dumb mode, elements producing no metalines still need to be
|
||||
# rendered as empty lines.
|
||||
if dumb:
|
||||
if options.mode == "dumb":
|
||||
if not element_metalines:
|
||||
element_metalines = [({"type": LineType.PARAGRAPH}, "")]
|
||||
# If current element requires margins and is not the first elements,
|
||||
|
@ -113,9 +122,9 @@ def generate_dumb_metalines(lines):
|
|||
return [({"type": LineType.PARAGRAPH}, line) for line in lines]
|
||||
|
||||
|
||||
def format_title(title: Title, context: dict):
|
||||
def format_title(title: Title, options: RenderOptions):
|
||||
"""Return metalines for this title."""
|
||||
width = context["width"]
|
||||
width = options.width
|
||||
if title.level == 1:
|
||||
wrapped = wrap_words(title.text, width)
|
||||
line_template = f"{{:^{width}}}"
|
||||
|
@ -129,19 +138,19 @@ def format_title(title: Title, context: dict):
|
|||
return [({"type": LineType(title.level)}, line) for line in lines]
|
||||
|
||||
|
||||
def format_paragraph(paragraph: Paragraph, context: dict):
|
||||
def format_paragraph(paragraph: Paragraph, options: RenderOptions):
|
||||
"""Return metalines for this paragraph."""
|
||||
lines = wrap_words(paragraph.text, context["width"])
|
||||
lines = wrap_words(paragraph.text, options.width)
|
||||
return [({"type": LineType.PARAGRAPH}, line) for line in lines]
|
||||
|
||||
|
||||
def format_link(link: Link, context: dict):
|
||||
def format_link(link: Link, options: RenderOptions):
|
||||
"""Return metalines for this link."""
|
||||
# Get a new link and build the "[id]" anchor.
|
||||
link_anchor = f"[{link.ident}] "
|
||||
link_text = link.text or link.url
|
||||
# Wrap lines, indented by the link anchor length.
|
||||
lines = wrap_words(link_text, context["width"], indent=len(link_anchor))
|
||||
lines = wrap_words(link_text, options.width, indent=len(link_anchor))
|
||||
first_line_meta = {
|
||||
"type": LineType.LINK,
|
||||
"url": link.url,
|
||||
|
@ -154,7 +163,7 @@ def format_link(link: Link, context: dict):
|
|||
return first_line + other_lines
|
||||
|
||||
|
||||
def format_preformatted(preformatted: Preformatted, context: dict):
|
||||
def format_preformatted(preformatted: Preformatted, options: RenderOptions):
|
||||
"""Return metalines for this preformatted block."""
|
||||
return [
|
||||
({"type": LineType.PREFORMATTED}, line)
|
||||
|
@ -162,17 +171,17 @@ def format_preformatted(preformatted: Preformatted, context: dict):
|
|||
]
|
||||
|
||||
|
||||
def format_blockquote(blockquote: Blockquote, context: dict):
|
||||
def format_blockquote(blockquote: Blockquote, options: RenderOptions):
|
||||
"""Return metalines for this blockquote."""
|
||||
lines = wrap_words(blockquote.text, context["width"], indent=2)
|
||||
lines = wrap_words(blockquote.text, options.width, indent=2)
|
||||
return [({"type": LineType.BLOCKQUOTE}, line) for line in lines]
|
||||
|
||||
|
||||
def format_list_item(item: ListItem, context: dict):
|
||||
def format_list_item(item: ListItem, options: RenderOptions):
|
||||
"""Return metalines for this list item."""
|
||||
indent = len(LIST_ITEM_MARK)
|
||||
lines = wrap_words(item.text, context["width"], indent=indent)
|
||||
first_line = LIST_ITEM_MARK + lines[0][indent:]
|
||||
indent = len(options.bullet)
|
||||
lines = wrap_words(item.text, options.width, indent=indent)
|
||||
first_line = options.bullet + lines[0][indent:]
|
||||
lines[0] = first_line
|
||||
return [({"type": LineType.LIST_ITEM}, line) for line in lines]
|
||||
|
||||
|
|
|
@ -2,11 +2,21 @@ from dataclasses import dataclass, field
|
|||
from typing import Optional
|
||||
|
||||
from bebop.gemtext import parse_gemtext
|
||||
from bebop.metalines import generate_dumb_metalines, generate_metalines
|
||||
from bebop.metalines import (
|
||||
RenderOptions, generate_dumb_metalines, generate_metalines)
|
||||
from bebop.mime import MimeType
|
||||
from bebop.links import Links
|
||||
|
||||
|
||||
def get_render_options(config: dict):
|
||||
"""Prepare RenderOptions from the user config."""
|
||||
return RenderOptions(
|
||||
width=config["text_width"],
|
||||
mode=config["render_mode"],
|
||||
bullet=config["list_item_bullet"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Page:
|
||||
"""Page-related data.
|
||||
|
@ -21,7 +31,7 @@ class Page:
|
|||
- title: optional page title.
|
||||
- mime: optional MIME type received from the server.
|
||||
- encoding: optional encoding received from the server.
|
||||
- render: optional render mode used to create the page from Gemtext.
|
||||
- render_opts: optional render options used to create the page from Gemtext.
|
||||
"""
|
||||
source: str
|
||||
metalines: list = field(default_factory=list)
|
||||
|
@ -29,15 +39,21 @@ class Page:
|
|||
title: str = ""
|
||||
mime: Optional[MimeType] = None
|
||||
encoding: str = ""
|
||||
render: Optional[str] = None
|
||||
render_opts: Optional[RenderOptions] = None
|
||||
|
||||
@staticmethod
|
||||
def from_gemtext(gemtext: str, wrap_at: int, render: str ="fancy"):
|
||||
def from_gemtext(gemtext: str, options: RenderOptions):
|
||||
"""Produce a Page from a Gemtext file or string."""
|
||||
dumb_mode = render == "dumb"
|
||||
elements, links, title = parse_gemtext(gemtext, dumb=dumb_mode)
|
||||
metalines = generate_metalines(elements, wrap_at, dumb=dumb_mode)
|
||||
return Page(gemtext, metalines, links, title, render=render)
|
||||
dumb = options.mode == "dumb"
|
||||
elements, links, title = parse_gemtext(gemtext, dumb=dumb)
|
||||
metalines = generate_metalines(elements, options)
|
||||
return Page(
|
||||
gemtext,
|
||||
metalines,
|
||||
links,
|
||||
title,
|
||||
render_opts=options
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str):
|
||||
|
|
|
@ -36,7 +36,7 @@ def save_capsule_prefs(prefs: dict, prefs_path: Path):
|
|||
return True
|
||||
|
||||
|
||||
def get_url_render_mode_pref(prefs: dict, url: str, default: str):
|
||||
def get_url_render_mode_pref(prefs: dict, url: str) -> Optional[str]:
|
||||
"""Return the desired render mode for this URL.
|
||||
|
||||
If the preferences contain the URL or a parent URL, the corresponding render
|
||||
|
@ -46,14 +46,13 @@ def get_url_render_mode_pref(prefs: dict, url: str, default: str):
|
|||
Arguments:
|
||||
- prefs: current capsule preferences.
|
||||
- url: URL about to be rendered.
|
||||
- default: default render mode if no user preferences match.
|
||||
"""
|
||||
prefix_urls = []
|
||||
for key in prefs:
|
||||
if url.startswith(key):
|
||||
prefix_urls.append(key)
|
||||
if not prefix_urls:
|
||||
return default
|
||||
return None
|
||||
key = max(prefix_urls, key=len)
|
||||
preference = prefs[key]
|
||||
return preference.get("render_mode", default)
|
||||
return preference.get("render_mode")
|
||||
|
|
|
@ -6,49 +6,41 @@ class TestPreferences(unittest.TestCase):
|
|||
|
||||
def test_get_url_render_mode_pref(self):
|
||||
prefs = {}
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
self.assertIsNone(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com",
|
||||
"default"
|
||||
), "default")
|
||||
))
|
||||
|
||||
prefs["gemini://example.com"] = {}
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
self.assertIsNone(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com",
|
||||
"default"
|
||||
), "default")
|
||||
))
|
||||
|
||||
prefs["gemini://example.com"] = {"render_mode": "test"}
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com",
|
||||
"default"
|
||||
), "test")
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com/path",
|
||||
"default"
|
||||
), "test")
|
||||
|
||||
prefs["gemini://example.com/specific/subdir"] = {"render_mode": "test2"}
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com/path",
|
||||
"default"
|
||||
), "test")
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com/specific",
|
||||
"default"
|
||||
), "test")
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com/specific/subdir",
|
||||
"default"
|
||||
), "test2")
|
||||
self.assertEqual(get_url_render_mode_pref(
|
||||
prefs,
|
||||
"gemini://example.com/specific/subdir/subsubdir",
|
||||
"default"
|
||||
), "test2")
|
||||
|
|
Reference in a new issue