diff --git a/CHANGES b/CHANGES
index 01b1dfc..ba07618 100644
--- a/CHANGES
+++ b/CHANGES
@@ -5,6 +5,7 @@ Changes
1.1.0 (*2021-02-??*)
====================
+- Add ABlog support
- Fix table generation with literal blocks
- Fix image copy when using glob
diff --git a/README.rst b/README.rst
index 87e7cb0..531cd4a 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,18 @@
sphinx_gemini_builder
#####################
-Build gemini blog from Sphinx.
+Build `Gemini `_ blog from
+`Sphinx `_ with
+`ABlog `_ compatibility.
+
+
+Gemini is a simple protocol between gopher and web. Sphinx is
+a documentation tool. This project builds Gemini capsule from
+Sphinx documentation. It supports ABlog extensions and manage
+Atom feeds.
+
+Installation and use
+--------------------
Install with `python setup.py install` and do `make gemini` in
your project.
@@ -9,9 +20,8 @@ your project.
You can add a `gemini_footer` in config, formatted under the
Gemini specification. You need to set `gemini_baseurl` for
-good url in links.
+good url in Atom feeds.
-TODO
-----
-- ablog support with RSS generation
+This project contains some parts of code from Sphinx and from
+ABlog.
diff --git a/setup.py b/setup.py
index 59e44f6..e6c8992 100644
--- a/setup.py
+++ b/setup.py
@@ -44,6 +44,7 @@ setup(
"sphinx_gemini_builder": [
"locale/*/LC_MESSAGES/*.po",
"locale/*/LC_MESSAGES/*.mo",
+ "templates/*.gmi",
]
},
entry_points={
diff --git a/sphinx_gemini_builder/__init__.py b/sphinx_gemini_builder/__init__.py
index 701c814..e6d1fab 100644
--- a/sphinx_gemini_builder/__init__.py
+++ b/sphinx_gemini_builder/__init__.py
@@ -44,6 +44,7 @@ class GeminiBuilder(TextBuilder):
def __init__(self, app) -> None:
super().__init__(app)
+ self.add_footer = True
self.baseurl = self.config.gemini_baseurl
self.images = []
@@ -101,9 +102,23 @@ class GeminiBuilder(TextBuilder):
def get_target_uri(self, docname: str, typ: str = None) -> str:
return self.baseurl + quote(docname) + self.out_suffix
+ def proxy_generate_atom_feeds(self):
+ if 'ablog' in self.config.extensions:
+ from .ablog_compatibility import generate_atom_feeds
+ self.add_footer = False
+ generate_atom_feeds(self)
+ self.add_footer = True
+
+ def proxy_generate_archive_pages(self):
+ if 'ablog' in self.config.extensions:
+ from .ablog_compatibility import generate_archive_pages
+ generate_archive_pages(self)
+
def finish(self) -> None:
self.finish_tasks.add_task(self.copy_image_files)
self.finish_tasks.add_task(self.copy_download_files)
+ self.finish_tasks.add_task(self.proxy_generate_archive_pages)
+ self.finish_tasks.add_task(self.proxy_generate_atom_feeds)
super().finish()
diff --git a/sphinx_gemini_builder/ablog_compatibility.py b/sphinx_gemini_builder/ablog_compatibility.py
new file mode 100644
index 0000000..c26fbe1
--- /dev/null
+++ b/sphinx_gemini_builder/ablog_compatibility.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+ablog compatibility
+"""
+
+import os
+import logging
+
+from feedgen.feed import FeedGenerator
+from sphinx.locale import _
+from sphinx.util.osutil import relative_uri
+from docutils.utils import new_document
+from docutils.io import StringOutput
+from docutils import nodes
+
+import ablog
+
+from ablog.blog import Blog, os_path_join, revise_pending_xrefs
+
+logger = logging.getLogger(__name__)
+text_type = str
+
+
+def to_gemini(builder, post, pagename, fulltext=False):
+ """
+ Convert post to gemini format
+ """
+ doctree = new_document("")
+ if fulltext:
+ deepcopy = post.doctree.deepcopy()
+ if isinstance(deepcopy, nodes.document):
+ doctree.extend(deepcopy.children)
+ else:
+ doctree.append(deepcopy)
+ else:
+ for node in post.excerpt:
+ doctree.append(node.deepcopy())
+ revise_pending_xrefs(doctree, pagename)
+ builder.env.resolve_references(doctree, pagename, builder)
+
+ destination = StringOutput(encoding="utf-8")
+
+ builder.secnumbers = {}
+ builder.imgpath = relative_uri(builder.get_target_uri(pagename), "_images")
+ builder.dlpath = relative_uri(builder.get_target_uri(pagename), "_downloads")
+ builder.current_docname = pagename
+ builder.writer.write(doctree, destination)
+ builder.writer.assemble_parts()
+ gemini = builder.writer.parts["whole"]
+ return gemini
+
+
+def generate_archive_pages(builder):
+ """
+ Generate archive pages for all posts, categories, tags, authors, and
+ drafts (from ablog).
+ """
+ if not ablog.builder_support(builder.app):
+ return
+
+ blog = Blog(builder.app)
+ for post in blog.posts:
+ for redirect in post.redirect:
+ yield (redirect, {"redirect": post.docname, "post": post}, "redirect.gmi")
+
+ found_docs = builder.env.found_docs
+ atom_feed = bool(blog.blog_baseurl)
+ feed_archives = blog.blog_feed_archives
+ blog_path = blog.blog_path
+ for title, header, catalog in [
+ (_("Authors"), _("Posts by"), blog.author),
+ (_("Locations"), _("Posts from"), blog.location),
+ (_("Languages"), _("Posts in"), blog.language),
+ (_("Categories"), _("Posts in"), blog.category),
+ (_("All posts"), _("Posted in"), blog.archive),
+ (_("Tags"), _("Posts tagged"), blog.tags),
+ ]:
+
+ if not catalog:
+ continue
+
+ context = {"parents": [], "title": title, "header": header, "catalog": catalog, "summary": True}
+ if catalog.docname not in found_docs:
+ yield (catalog.docname, context, "catalog.gmi")
+
+ for collection in catalog:
+
+ if not collection:
+ continue
+ context = {
+ "parents": [],
+ "title": f"{header} {collection}",
+ "header": header,
+ "collection": collection,
+ "summary": True,
+ "feed_path": collection.path if feed_archives else blog_path,
+ "archive_feed": atom_feed and feed_archives,
+ }
+ context["feed_title"] = context["title"]
+ if collection.docname not in found_docs:
+ yield (collection.docname, context, "collection.gmi")
+
+ context = {
+ "parents": [],
+ "title": _("All Posts"),
+ "header": _("All"),
+ "collection": blog.posts,
+ "summary": True,
+ "atom_feed": atom_feed,
+ "feed_path": blog.blog_path,
+ }
+ docname = blog.posts.docname
+ yield (docname, context, "collection.gmi")
+
+ context = {"parents": [], "title": _("Drafts"), "collection": blog.drafts, "summary": True}
+ yield (blog.drafts.docname, context, "collection.gmi")
+
+
+def generate_atom_feeds(builder):
+ """
+ Generate archive pages for all posts, categories, tags, authors, and
+ drafts (from ablog).
+ """
+ blog = Blog(builder.app)
+
+ url = blog.blog_baseurl
+ if not url:
+ return
+
+ feed_path = os.path.join(builder.outdir, blog.blog_path, "atom.xml")
+
+ feeds = [
+ (
+ blog.posts,
+ blog.blog_path,
+ feed_path,
+ blog.blog_title,
+ os_path_join(url, blog.blog_path, "atom.xml"),
+ )
+ ]
+
+ if blog.blog_feed_archives:
+ for header, catalog in [
+ (_("Posts by"), blog.author),
+ (_("Posts from"), blog.location),
+ (_("Posts in"), blog.language),
+ (_("Posts in"), blog.category),
+ (_("Posted in"), blog.archive),
+ (_("Posts tagged"), blog.tags),
+ ]:
+
+ for coll in catalog:
+ # skip collections containing only drafts
+ if not len(coll):
+ continue
+ folder = os.path.join(builder.outdir, coll.path)
+ if not os.path.isdir(folder):
+ os.makedirs(folder)
+
+ feeds.append(
+ (
+ coll,
+ coll.path,
+ os.path.join(folder, "atom.xml"),
+ blog.blog_title + " - " + header + " " + text_type(coll),
+ os_path_join(url, coll.path, "atom.xml"),
+ )
+ )
+
+ # Config options
+ feed_length = blog.blog_feed_length
+ feed_fulltext = blog.blog_feed_fulltext
+
+ for feed_posts, pagename, feed_path, feed_title, feed_url in feeds:
+
+ feed = FeedGenerator()
+ feed.id(blog.blog_baseurl)
+ feed.title(feed_title)
+ feed.link(href=url)
+ feed.subtitle(blog.blog_feed_subtitle)
+ feed.link(href=feed_url)
+ feed.language(builder.config.language)
+ feed.generator("ABlog", ablog.__version__, "https://ablog.readthedocs.org")
+
+ for i, post in enumerate(feed_posts):
+ if feed_length and i == feed_length:
+ break
+ post_url = os_path_join(url, builder.get_target_uri(post.docname))
+
+ if blog.blog_feed_titles:
+ content = None
+ else:
+ content = to_gemini(builder, post, pagename, fulltext=feed_fulltext)
+
+ feed_entry = feed.add_entry()
+ feed_entry.id(post_url)
+ feed_entry.title(post.title)
+ feed_entry.link(href=post_url)
+ feed_entry.author({"name": author.name for author in post.author})
+ feed_entry.pubDate(post.date.astimezone())
+ feed_entry.updated(post.update.astimezone())
+ feed_entry.content(content=content, type="text/gemini")
+
+ parent_dir = os.path.dirname(feed_path)
+ if not os.path.isdir(parent_dir):
+ os.makedirs(parent_dir)
+
+ with open(feed_path, "w", encoding="utf-8") as out:
+ feed_str = feed.atom_str(pretty=True)
+ out.write(feed_str.decode())
diff --git a/sphinx_gemini_builder/writer.py b/sphinx_gemini_builder/writer.py
index 081cca6..132d6f6 100644
--- a/sphinx_gemini_builder/writer.py
+++ b/sphinx_gemini_builder/writer.py
@@ -96,7 +96,8 @@ class GeminiTranslator(SphinxTranslator):
pass
def depart_document(self, node: Element) -> None:
- self.add_text(self.config.gemini_footer)
+ if self.builder.add_footer:
+ self.add_text(self.config.gemini_footer)
def visit_section(self, node: Element) -> None:
self.sectionlevel += 1