Development =========== Nefelibata is written in Python 3.9, and has been tested with Python 3.7 and 3.8 as well. It has a modularized plugin architecture, allowing new features to be easily added. Plugins are discovered through `entry points `_, allowing anyone to write custom Python libraries that extend nefelibata. There are 4 types of plugins: builders Builders are used to generate HTML from the Markdown files. They generate the index page, tags, archives and Atom feed. assistants Assistants are HTML post-processors, and run on the HTML generated by builders. They mirror images locally, save external links on the `Wayback Machine `_, and more. publishers Publishers will upload the static files to one or more hosting services. Currently `AWS S3 `_, `Neocities `_, FTP, and `IPFS `_ are supported. announcers Announcers will syndicate your posts externally, linking back to the post. Currently `Twitter `_, `Mastodon `_, `Medium `_ and a few more are supported. There is also a special announcer to handle `webmentions `_. Builders -------- This is the base builder class: .. code-block:: python class Builder: scopes: List[Scope] = [] def __init__(self, root: Path, config: Dict[str, Any], *args: Any, **kwargs: Any): self.root = root self.config = config def process_post(self, post: Post, force: bool = False) -> None: if Scope.POST not in self.scopes: raise Exception(f'Scope "post" not supported by {self.__class__.__name__}') raise NotImplementedError("Subclasses MUST implement `process_post`") def process_site(self, force: bool = False) -> None: if Scope.SITE not in self.scopes: raise Exception(f'Scope "site" not supported by {self.__class__.__name__}') raise NotImplementedError("Subclasses MUST implement `process_site`") A builder has one or more **scopes**, and they can be either "post" or "site". The post builder, eg, has only the "post" scope. It defines a ``process_post`` method that takes a ``Post`` object and writes an HTML file next to it. The index builder, on the other hand, has only the "site" scope, and defines a ``process_site`` method that builds a feed from the last 10 posts. If we wanted to have a feed for comments in each post we could add the "post" scope, and add a ``process_post`` method that generates an Atom feed for each post. Assistants ---------- Assistants are very similar to builders, and they have the exact same interface — they are a subclass of ``Builder``. The main difference is that builders work on the post Markdown, while assistants work on the generated HTML. For example, there is an assistant that extracts all links to external images, and downloads them locally. Publishers ---------- A publisher is a class that defines a ``publish`` method. Here's a simple example: .. code-block:: python from pathlib import Path from typing import List from nefelibata.publishers import Publisher class MyPublisher(Publisher) def __init__(self, root: Path, config: Dict[str, Any], secret_code: str): super().__init__(root, config) self.secret_code = secret_code def publish(self, force: bool = False) -> None: # store file with the last time weblog was published last_published_file = self.root / "last_published" if last_published_file.exists(): last_published = last_published_file.stat().st_mtime else: last_published = 0 modified_files: List[Path] = self.find_modified_files(force, since=last_published) for file in modified_files: pass # upload # update last published last_published_file.touch() To use the custom publisher users would add this to their ``nefelibata.yaml``: .. code-block:: yaml publish-to: - my_publisher my_publisher: secret_code: This assumes that the publisher is exposed through an entry point: .. code-block:: ini nefelibata.publisher = my_publisher = nefelibata.publishers.my_publisher:MyPublisher Note that entry points are package agnostic. If you want to add a new plugin you can submit a pull request to nefelibata, but you can also create a new package declaring the entry point and nefelibata will pick it up automatically. Announcers ---------- Announcers are responsible for two main tasks: publishing a post somewhere else, and fetching replies to it. These are performed by two methods, ``announce`` and ``collect``, respectively: .. code-block:: python class MyAnnouncer(Announcer): id = "my_announcer" name = "My Announcer" url_header = "my-announcer-url" def announce(self, post: Post) -> Optional[str]: """Publish the post and return the URL where it was published.""" pass def collect(self, post: Post) -> List[Response]: """Colect all responses made to the post.""" pass If you're interested in making your own announcer, the Mastodon announcer is a good example, since the API is straightforward. Like publishers, announcers can also take extra instantiation arguments that are defined in ``nefelibata.yaml`` and passed through the ``__init__`` method.