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.
This commit is contained in:
2026-08-12 21:57:57 +02:00
parent 61bc762256
commit f1451a17b4
4 changed files with 113 additions and 67 deletions
+3
View File
@@ -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",
]
+97
View File
@@ -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
+5 -59
View File
@@ -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)
+8 -8
View File
@@ -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" %}
<nav aria-label="breadcrumb">
<a href="{{ site.base_dir }}">{{ site.name }}</a>
{{ section }}
{{ label }}
</nav>
{% elif tag %}
{% elif kind == "tag" %}
<nav aria-label="breadcrumb">
<a href="{{ site.base_dir }}">{{ site.name }}</a>
<a href="{{ site.base_dir }}tags/">Tags</a>
{{ tag }}
{{ label }}
</nav>
{% else %}
{{ super() }}
@@ -40,11 +40,11 @@
{% if total_pages > 1 %}
<nav>
{% if page_num > 1 %}
<a href="{{ site.base_dir }}{{ prefix }}page-{{ page_num - 1 }}.html">← Newer</a>
<a href="{{ site.base_dir }}{{ prefix }}{{ "" if page_num - 1 == 1 else "page-" ~ (page_num - 1) ~ ".html" }}">← Newer</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page_num %}<strong>{{ p }}</strong>
{% else %}<a href="{{ site.base_dir }}{{ prefix }}page-{{ p }}.html">{{ p }}</a>
{% else %}<a href="{{ site.base_dir }}{{ prefix }}{{ "" if p == 1 else "page-" ~ p ~ ".html" }}">{{ p }}</a>
{% endif %}
{% endfor %}
{% if page_num < total_pages %}