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:
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user