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.output import Output
from omicron.ssg.output.content import Content, ContentError from omicron.ssg.output.content import Content, ContentError
from omicron.ssg.output.factory import create_content, is_content 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 from omicron.ssg.output.memory import Memory
__all__ = [ __all__ = [
@@ -9,5 +10,7 @@ __all__ = [
"ContentError", "ContentError",
"create_content", "create_content",
"is_content", "is_content",
"Index",
"discover_index",
"Memory", "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 import logging
from typing import Any, cast from typing import Any, cast
from urllib.parse import urlparse from urllib.parse import urlparse
from itertools import batched
from math import ceil
from omicron.ssg.output import ( from omicron.ssg.output import (
Output, Output,
@@ -11,6 +9,7 @@ from omicron.ssg.output import (
is_content, is_content,
create_content, create_content,
Memory, Memory,
discover_index,
) )
from omicron.ssg.markdown import highlight_style from omicron.ssg.markdown import highlight_style
@@ -118,51 +117,11 @@ class Site:
self.add_by_tag(content) self.add_by_tag(content)
self.content.append(content) self.content.append(content)
self.update_tags_by_count() 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)) 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: def render_tags_page(self) -> None:
template = self.jinja_env.get_template("tags.html") template = self.jinja_env.get_template("tags.html")
html = template.render(site=self, tags=self.tags_by_count) 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.mkdir(parents=True, exist_ok=True)
(tags_dir / "index.html").write_text(html, encoding="utf-8") (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: def build(self) -> None:
if not self.origin: if not self.origin:
log.warning( log.warning(
@@ -195,6 +141,6 @@ class Site:
self.output_path.mkdir(parents=True, exist_ok=True) self.output_path.mkdir(parents=True, exist_ok=True)
for content in self.content: for content in self.content:
content.write(self) content.write(self)
self.render_indices() self.render_tags_page()
for output in self.other_outputs: for output in self.other_outputs:
output.write(self) output.write(self)
+8 -8
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% set prefix = (section + "/") if section else (("tags/" + tag + "/") if tag else "") %} {% set prefix = (label + "/") if kind == "section" else (("tags/" + label + "/") if kind == "tag" else "") %}
{% set heading = section or tag or "" %} {% set heading = label or "" %}
{% block title %}{% if heading %}{{ heading }} — {% endif %}{{ site.name }}{% if page_num > 1 %} — Page {{ page_num }}{% endif %}{% endblock %} {% block title %}{% if heading %}{{ heading }} — {% endif %}{{ site.name }}{% if page_num > 1 %} — Page {{ page_num }}{% endif %}{% endblock %}
@@ -10,16 +10,16 @@
{% endblock %} {% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
{% if section %} {% if kind == "section" %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir }}">{{ site.name }}</a> <a href="{{ site.base_dir }}">{{ site.name }}</a>
{{ section }} {{ label }}
</nav> </nav>
{% elif tag %} {% elif kind == "tag" %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<a href="{{ site.base_dir }}">{{ site.name }}</a> <a href="{{ site.base_dir }}">{{ site.name }}</a>
<a href="{{ site.base_dir }}tags/">Tags</a> <a href="{{ site.base_dir }}tags/">Tags</a>
{{ tag }} {{ label }}
</nav> </nav>
{% else %} {% else %}
{{ super() }} {{ super() }}
@@ -40,11 +40,11 @@
{% if total_pages > 1 %} {% if total_pages > 1 %}
<nav> <nav>
{% if page_num > 1 %} {% 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 %} {% endif %}
{% for p in range(1, total_pages + 1) %} {% for p in range(1, total_pages + 1) %}
{% if p == page_num %}<strong>{{ p }}</strong> {% 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 %} {% endif %}
{% endfor %} {% endfor %}
{% if page_num < total_pages %} {% if page_num < total_pages %}