The following changes have been made all with template cleanup in mind: - Make Output.url relative to the origin root - Keep weakref to Site in all Output objects. - Add several helpers to get urls from Site: home_url(), tag_url(tag), section_url(section) and tags_url(). - Add helpers to Index to get pagination urls: pagination_url(n) - Clean up templates by making use of these new facilities
171 lines
6.0 KiB
Python
171 lines
6.0 KiB
Python
import logging
|
|
from typing import Any, cast
|
|
from urllib.parse import urlparse
|
|
|
|
from omicron.ssg.output import (
|
|
Output,
|
|
Content,
|
|
ContentError,
|
|
is_content,
|
|
create_content,
|
|
File,
|
|
Index,
|
|
Tags,
|
|
Memory,
|
|
discover_index,
|
|
discover_tags,
|
|
)
|
|
from omicron.ssg.markdown import highlight_style
|
|
|
|
from pathlib import Path
|
|
import yaml
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ConfigError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class Site:
|
|
def __init__(self, site: Path):
|
|
self.site_path = site.absolute()
|
|
self.content: list[Content] = []
|
|
self.other_outputs: list[Output] = []
|
|
self.by_tag: dict[str, list[Content]] = {}
|
|
self.by_path: dict[Path, Output] = {}
|
|
self.by_section: dict[str, list[Content]] = {}
|
|
self.tags_by_count: list[tuple[str, int]] = []
|
|
|
|
config = self.read_config()
|
|
self.name: str = config["name"]
|
|
self.template: str = config["template"]
|
|
self.items_per_page: int = int(config.get("items_per_page", 20))
|
|
self.origin, self.base_dir = Site.parse_base_url(config.get("base_url"))
|
|
self.template_path: Path = Site.resolve_template_path(self.template)
|
|
self.output_path: Path = self.site_path / "output"
|
|
self.jinja_env = Environment(loader=FileSystemLoader(self.template_path))
|
|
|
|
def read_config(self) -> dict[str, Any]:
|
|
config_path = self.site_path / "config.yml"
|
|
log.debug("Reading config file %s", config_path)
|
|
with open(config_path, "r") as f:
|
|
config = yaml.safe_load(f)
|
|
if not isinstance(config, dict) or not all(isinstance(k, str) for k in config):
|
|
raise ConfigError("config file must be a YAML mapping with string keys")
|
|
if "name" not in config:
|
|
raise ConfigError("name value missing from config")
|
|
if "template" not in config:
|
|
raise ConfigError("template value missing from config")
|
|
return cast(dict[str, Any], config)
|
|
|
|
@staticmethod
|
|
def parse_base_url(value: str | None) -> tuple[str, str]:
|
|
if not value:
|
|
return ("", "")
|
|
parsed = urlparse(value)
|
|
if parsed.scheme and parsed.netloc:
|
|
origin = f"{parsed.scheme}://{parsed.netloc}"
|
|
elif parsed.netloc:
|
|
origin = f"//{parsed.netloc}"
|
|
else:
|
|
origin = ""
|
|
|
|
path = parsed.path.removesuffix("/")
|
|
return (origin, path)
|
|
|
|
@staticmethod
|
|
def resolve_template_path(template: str) -> Path:
|
|
path = Path(__file__).parent / "templates" / template
|
|
if not path.is_dir():
|
|
raise ConfigError(f"template '{template}' not found at {path}")
|
|
return path
|
|
|
|
def add_by_section(self, content: Content) -> None:
|
|
if content.section not in self.by_section:
|
|
self.by_section[content.section] = []
|
|
self.by_section[content.section].append(content)
|
|
|
|
def add_by_tag(self, content: Content) -> None:
|
|
for tag in content.tags:
|
|
if tag not in self.by_tag:
|
|
self.by_tag[tag] = []
|
|
self.by_tag[tag].append(content)
|
|
|
|
def add_by_path(self, output: Output) -> None:
|
|
if output.destination in self.by_path:
|
|
raise ContentError(
|
|
f"path '{output.destination}' is already claimed by another output"
|
|
)
|
|
self.by_path[output.destination] = output
|
|
|
|
def home_url(self) -> str:
|
|
return self.base_dir or "/"
|
|
|
|
def section_url(self, section: str) -> str:
|
|
path = Index.build_path("section", 1, section)
|
|
return self.by_path[path].url
|
|
|
|
def tag_url(self, tag: str) -> str:
|
|
path = Index.build_path("tag", 1, tag)
|
|
return self.by_path[path].url
|
|
|
|
def tags_url(self) -> str:
|
|
return self.by_path[Tags.PATH].url
|
|
|
|
def update_tags_by_count(self) -> None:
|
|
tags = [(tag, len(items)) for tag, items in self.by_tag.items()]
|
|
tags.sort(key=lambda x: x[1], reverse=True)
|
|
self.tags_by_count = tags
|
|
|
|
def discover_assets(self) -> None:
|
|
assets_path = self.template_path / "assets"
|
|
if not assets_path.is_dir():
|
|
return
|
|
for file in assets_path.rglob("*"):
|
|
if file.is_file():
|
|
path = Path("assets") / file.relative_to(assets_path)
|
|
asset = File(self, path, file)
|
|
self.add_by_path(asset)
|
|
self.other_outputs.append(asset)
|
|
log.debug("Discovered template asset %s", path)
|
|
|
|
def discover(self) -> None:
|
|
log.info("Discovering content...")
|
|
self.discover_assets()
|
|
pygments = Memory(self, Path("assets/pygments.css"), highlight_style())
|
|
self.other_outputs.append(pygments)
|
|
self.add_by_path(pygments)
|
|
content_path = self.site_path / "content"
|
|
for file in content_path.rglob("*"):
|
|
if file.is_file() and is_content(file):
|
|
content = create_content(self, file)
|
|
self.add_by_path(content)
|
|
self.add_by_section(content)
|
|
self.add_by_tag(content)
|
|
self.content.append(content)
|
|
self.update_tags_by_count()
|
|
for index in discover_index(self):
|
|
self.add_by_path(index)
|
|
self.other_outputs.append(index)
|
|
tags_page = discover_tags(self)
|
|
self.add_by_path(tags_page)
|
|
self.other_outputs.append(tags_page)
|
|
log.info("Discovered %d content items", len(self.content))
|
|
|
|
def build(self) -> None:
|
|
if not self.origin:
|
|
log.warning(
|
|
"base_url config value is missing a domain name, "
|
|
"can't add canonical url to content"
|
|
)
|
|
if self.base_dir and not self.base_dir.startswith("/"):
|
|
raise ConfigError("base_url config value must be an absolute path")
|
|
self.discover()
|
|
self.output_path.mkdir(parents=True, exist_ok=True)
|
|
for content in self.content:
|
|
content.write()
|
|
for output in self.other_outputs:
|
|
output.write()
|