From 7ed83f93891df85230f80210c42419759ac2016f Mon Sep 17 00:00:00 2001 From: dece Date: Mon, 28 Jun 2021 00:49:44 +0200 Subject: [PATCH] page: rework rendering params into RenderOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also make the list item bullet configurable, if you don't like the •. --- bebop/browser/browser.py | 22 ++++++------ bebop/browser/file.py | 8 ++--- bebop/browser/gemini.py | 16 ++++----- bebop/config.py | 3 +- bebop/help.py | 1 + bebop/metalines.py | 59 +++++++++++++++++++-------------- bebop/page.py | 32 +++++++++++++----- bebop/preferences.py | 7 ++-- bebop/tests/test_preferences.py | 16 +++------ 9 files changed, 88 insertions(+), 76 deletions(-) diff --git a/bebop/browser/browser.py b/bebop/browser/browser.py index b77b3ed..5a99751 100644 --- a/bebop/browser/browser.py +++ b/bebop/browser/browser.py @@ -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}'.") diff --git a/bebop/browser/file.py b/bebop/browser/file.py index ffb0306..a5bcf43 100644 --- a/bebop/browser/file.py +++ b/bebop/browser/file.py @@ -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 diff --git a/bebop/browser/gemini.py b/bebop/browser/gemini.py index 6a86069..1b9bf33 100644 --- a/bebop/browser/gemini.py +++ b/bebop/browser/gemini.py @@ -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") diff --git a/bebop/config.py b/bebop/config.py index f69f452..77d3780 100644 --- a/bebop/config.py +++ b/bebop/config.py @@ -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(): diff --git a/bebop/help.py b/bebop/help.py index 955eeac..8258196 100644 --- a/bebop/help.py +++ b/bebop/help.py @@ -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. diff --git a/bebop/metalines.py b/bebop/metalines.py index 730053c..5a64107 100644 --- a/bebop/metalines.py +++ b/bebop/metalines.py @@ -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] diff --git a/bebop/page.py b/bebop/page.py index f75ed3e..c16e53e 100644 --- a/bebop/page.py +++ b/bebop/page.py @@ -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): diff --git a/bebop/preferences.py b/bebop/preferences.py index 46de625..e75c22b 100644 --- a/bebop/preferences.py +++ b/bebop/preferences.py @@ -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") diff --git a/bebop/tests/test_preferences.py b/bebop/tests/test_preferences.py index 5a38810..f47c040 100644 --- a/bebop/tests/test_preferences.py +++ b/bebop/tests/test_preferences.py @@ -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")