From f1451a17b453a73a694acbe2e8efed4fc29ee0cc Mon Sep 17 00:00:00 2001 From: omicron Date: Wed, 12 Aug 2026 18:18:15 +0200 Subject: [PATCH] Create Index(Output) class and migrate index generation from Site Like previous Content refactor, this allows validating file collisions in the discover/write build pipeline. --- omicron/ssg/output/__init__.py | 3 + omicron/ssg/output/index.py | 97 ++++++++++++++++++++++++++ omicron/ssg/site.py | 64 ++--------------- omicron/ssg/templates/plain/index.html | 16 ++--- 4 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 omicron/ssg/output/index.py diff --git a/omicron/ssg/output/__init__.py b/omicron/ssg/output/__init__.py index 5c21e28..07c1d7c 100644 --- a/omicron/ssg/output/__init__.py +++ b/omicron/ssg/output/__init__.py @@ -1,6 +1,7 @@ from omicron.ssg.output.output import Output from omicron.ssg.output.content import Content, ContentError from omicron.ssg.output.factory import create_content, is_content +from omicron.ssg.output.index import Index, discover_index from omicron.ssg.output.memory import Memory __all__ = [ @@ -9,5 +10,7 @@ __all__ = [ "ContentError", "create_content", "is_content", + "Index", + "discover_index", "Memory", ] diff --git a/omicron/ssg/output/index.py b/omicron/ssg/output/index.py new file mode 100644 index 0000000..a169979 --- /dev/null +++ b/omicron/ssg/output/index.py @@ -0,0 +1,97 @@ +import logging +from math import ceil +from itertools import batched +from typing import TYPE_CHECKING, Literal +from omicron.ssg.output.output import Output +from omicron.ssg.output.content import Content + +log = logging.getLogger(__name__) + +if TYPE_CHECKING: + from omicron.ssg.site import Site # for static checking with mypy +else: + Site = "omicron.ssg.site.Site" # for runtime checking with beartype + + +class Index(Output): + def __init__( + self, + kind: Literal["root", "section", "tag"], + items: list[Content], + page_num: int, + total_pages: int, + label: str | None, + ): + super().__init__(Index.build_uri(kind, page_num, label)) + self.kind = kind + self.items = items + self.page_num = page_num + self.total_pages = total_pages + self.label = label + + @staticmethod + def build_uri( + kind: Literal["root", "section", "tag"], + page_num: int, + label: str | None, + ) -> str: + if page_num == 1: + file = "index.html" + else: + file = f"page-{page_num}.html" + + if kind == "root": + uri = file + elif kind == "section": + assert label, "label can't be None if kind is not root" + uri = f"{label}/{file}" + else: + assert label, "label can't be None if kind is not root" + uri = f"tags/{label}/{file}" + return uri + + def write(self, site: Site) -> None: + template = site.jinja_env.get_template("index.html") + html = template.render( + site=site, + items=self.items, + page_num=self.page_num, + total_pages=self.total_pages, + kind=self.kind, + label=self.label, + ) + dest = site.output_path / self.uri + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(html, encoding="utf-8") + log.debug("Wrote %s", dest) + + +def make_index_pages( + items: list[Content], + kind: Literal["root", "section", "tag"], + label: str | None, + items_per_page: int, +) -> list[Index]: + sorted_items = sorted(items, key=lambda c: c.created, reverse=True) + if not sorted_items: + log.warning( + "No content for %s index%s, generating empty page", + kind, + f" ({label})" if label else "", + ) + return [Index(kind, [], 1, 1, label)] + num_pages = ceil(len(sorted_items) / items_per_page) + return [ + Index(kind, list(page_items), i + 1, num_pages, label) + for i, page_items in enumerate(batched(sorted_items, items_per_page)) + ] + + +def discover_index(site: Site) -> list[Index]: + outputs: list[Index] = [] + outputs.extend(make_index_pages(site.content, "root", None, site.items_per_page)) + for section, items in site.by_section.items(): + outputs.extend(make_index_pages(items, "section", section, site.items_per_page)) + for tag, items in site.by_tag.items(): + outputs.extend(make_index_pages(items, "tag", tag, site.items_per_page)) + return outputs diff --git a/omicron/ssg/site.py b/omicron/ssg/site.py index 91b9670..a67ec28 100644 --- a/omicron/ssg/site.py +++ b/omicron/ssg/site.py @@ -1,8 +1,6 @@ import logging from typing import Any, cast from urllib.parse import urlparse -from itertools import batched -from math import ceil from omicron.ssg.output import ( Output, @@ -11,6 +9,7 @@ from omicron.ssg.output import ( is_content, create_content, Memory, + discover_index, ) from omicron.ssg.markdown import highlight_style @@ -118,51 +117,11 @@ class Site: self.add_by_tag(content) self.content.append(content) self.update_tags_by_count() + for index in discover_index(self): + self.add_by_uri(index) + self.other_outputs.append(index) log.info("Discovered %d content items", len(self.content)) - def render_single_index_page( - self, - out_dir: Path, - items: tuple[Content, ...], - page_num: int, - total_pages: int, - section: str | None, - tag: str | None = None, - ) -> None: - template = self.jinja_env.get_template("index.html") - html = template.render( - site=self, - items=items, - page_num=page_num, - total_pages=total_pages, - section=section, - tag=tag, - ) - out_file = out_dir / f"page-{page_num}.html" - out_file.write_text(html, encoding="utf-8") - - def render_index_pages( - self, - items: list[Content], - out_dir: Path, - section: str | None, - tag: str | None = None, - ) -> None: - out_dir.mkdir(parents=True, exist_ok=True) - if not items: - log.warning("No content, generating empty index page") - self.render_single_index_page(out_dir, (), 1, 1, section, tag) - else: - num_pages = ceil(len(items) / self.items_per_page) - for i, page_items in enumerate(batched(items, self.items_per_page)): - self.render_single_index_page( - out_dir, page_items, i + 1, num_pages, section, tag - ) - index = out_dir / "index.html" - if index.exists() or index.is_symlink(): - index.unlink() - index.symlink_to("page-1.html") - def render_tags_page(self) -> None: template = self.jinja_env.get_template("tags.html") html = template.render(site=self, tags=self.tags_by_count) @@ -170,19 +129,6 @@ class Site: tags_dir.mkdir(parents=True, exist_ok=True) (tags_dir / "index.html").write_text(html, encoding="utf-8") - def render_indices(self) -> None: - sorted_all = sorted(self.content, key=lambda c: c.created, reverse=True) - self.render_index_pages(sorted_all, self.output_path, None) - for section, items in self.by_section.items(): - sorted_items = sorted(items, key=lambda c: c.created, reverse=True) - self.render_index_pages(sorted_items, self.output_path / section, section) - for tag, items in self.by_tag.items(): - sorted_items = sorted(items, key=lambda c: c.created, reverse=True) - self.render_index_pages( - sorted_items, self.output_path / "tags" / tag, None, tag - ) - self.render_tags_page() - def build(self) -> None: if not self.origin: log.warning( @@ -195,6 +141,6 @@ class Site: self.output_path.mkdir(parents=True, exist_ok=True) for content in self.content: content.write(self) - self.render_indices() + self.render_tags_page() for output in self.other_outputs: output.write(self) diff --git a/omicron/ssg/templates/plain/index.html b/omicron/ssg/templates/plain/index.html index 4b78e5b..2f6e023 100644 --- a/omicron/ssg/templates/plain/index.html +++ b/omicron/ssg/templates/plain/index.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% set prefix = (section + "/") if section else (("tags/" + tag + "/") if tag else "") %} -{% set heading = section or tag or "" %} +{% set prefix = (label + "/") if kind == "section" else (("tags/" + label + "/") if kind == "tag" else "") %} +{% set heading = label or "" %} {% block title %}{% if heading %}{{ heading }} — {% endif %}{{ site.name }}{% if page_num > 1 %} — Page {{ page_num }}{% endif %}{% endblock %} @@ -10,16 +10,16 @@ {% endblock %} {% block breadcrumb %} -{% if section %} +{% if kind == "section" %} -{% elif tag %} +{% elif kind == "tag" %} {% else %} {{ super() }} @@ -40,11 +40,11 @@ {% if total_pages > 1 %}