From a05368c8c8f9b97d727dc8d2efcf847743b29f66 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Fri, 20 Aug 2021 11:58:42 +0200 Subject: implement Gemtext format --- wikimini/document.py | 16 ++++++------ wikimini/formats/__init__.py | 47 ++++++++++++++++++++++++++++++++--- wikimini/formats/gemtext.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 wikimini/formats/gemtext.py diff --git a/wikimini/document.py b/wikimini/document.py index 7c459b6..1e18a8b 100644 --- a/wikimini/document.py +++ b/wikimini/document.py @@ -325,22 +325,22 @@ class BlockQuote(Block): """A quote. Attributes: - nodes: The content of the blockquote. + content: The content of the blockquote. """ - __slots__ = ("nodes",) - nodes: Paragraph + __slots__ = ("content",) + content: Paragraph def __bool__(self): - return bool(self.nodes) + return bool(self.content) def append(self, node): - self.nodes.append(node) + self.content.append(node) def plain(self): - return self.nodes.plain() + return self.content.plain() - def to_nodes(self): - return self.nodes.to_nodes() + def to_content(self): + return self.content.to_nodes() @dataclass diff --git a/wikimini/formats/__init__.py b/wikimini/formats/__init__.py index 8d6296c..b48486a 100644 --- a/wikimini/formats/__init__.py +++ b/wikimini/formats/__init__.py @@ -4,7 +4,8 @@ Formats work by being given a file-like buffer as argument, into which the output should be written. """ -from typing import TextIO +import io +from typing import TextIO, Union from ..document import ( Document, Block, BlockLink, BlockQuote, Heading, ItemList, LineBreak, @@ -18,9 +19,9 @@ class Format: Any output format should inherit from this class and override the specific output methods. Note that by default, no output is generated. - The methods :meth:`render_document`, :meth:`render_block` and - :meth:`render_node` have sensible default implementations that dispatch to - the more specific rendering methods. + The methods :meth:`render`, :meth:`render_document`, :meth:`render_block` + and :meth:`render_node` have sensible default implementations that dispatch + to the more specific rendering methods. Attributes: writer: The file-like object that output should be written to. @@ -30,6 +31,21 @@ class Format: def __init__(self, writer: TextIO): self.writer = writer + def render(self, obj: Union[Document, Block, Node]): + """Renders the given object. + + Args: + obj: The object to render. + """ + if isinstance(obj, Document): + self.render_document(obj) + elif isinstance(obj, Block): + self.render_block(obj) + elif isinstance(obj, Node): + self.render_node(obj) + else: + raise TypeError(f"Cannot render {obj}, unknown type") + def render_document(self, document: Document): """Renders the given document. @@ -146,3 +162,26 @@ class Format: Args: style: The styled text to render. """ + + +def as_string(formatter: Format, obj: Union[Document, Node, Block]) -> str: + """Runs the given format function and returns the result as a string. + + This temporarily replaces the output writer by an in-memory string object, + runs the render function and then restores the writer. + + Args: + formatter: The formatter to run. + obj: The object to render. + + Returns: + The content, as string. + """ + old_writer = formatter.writer + buffer = io.StringIO() + formatter.writer = buffer + try: + formatter.render(obj) + finally: + formatter.writer = old_writer + return buffer.getvalue() diff --git a/wikimini/formats/gemtext.py b/wikimini/formats/gemtext.py new file mode 100644 index 0000000..935565c --- /dev/null +++ b/wikimini/formats/gemtext.py @@ -0,0 +1,58 @@ +"""This module contains a Gemtext formatter for +:class:`~wikimini.document.Document`. +""" +from itertools import zip_longest +from . import Format, as_string +from ..document import LineBreak, BlockLink, InlineLink + + +class Gemtext(Format): + """The Gemtext formatter.""" + + def render_document(self, document): + for block, next_block in zip_longest( + document.blocks, document.blocks[1:]): + self.render_block(block) + if not isinstance(next_block, (LineBreak, BlockLink)): + self.writer.write("\n") + + def render_block_link(self, block_link): + self.writer.write(f"=> {block_link.href} {block_link.title}\n") + + def render_block_quote(self, block_quote): + content = as_string(self, block_quote.content) + for line in content.split("\n"): + self.writer.write(f"> {line}\n") + + def render_heading(self, heading): + level = min(3, heading.level) + self.writer.write("#" * level + f" {heading.text}\n") + + def render_inline_link(self, inline_link): + self.render(inline_link.title) + + def render_item_list(self, item_list): + for item in item_list.items: + if len(item.nodes) == 1 and isinstance(item.nodes[0], InlineLink): + link = item.nodes[0] + self.render(BlockLink(link.href, link.title.plain())) + else: + self.writer.write("* ") + self.render(item) + + def render_line_break(self, _): + self.writer.write("\n") + + def render_paragraph(self, paragraph): + for node in paragraph.nodes: + self.render(node) + self.writer.write("\n") + + def render_plain(self, plain): + self.writer.write(plain.text) + + def render_style(self, style): + self.render(style.inner) + + def render_verbatim(self, verbatim): + self.writer.write(f"```\n{verbatim.text}\n```\n") -- cgit v1.2.3