This commit is contained in:
parent
4f8979ae9b
commit
1bd5225fb6
8 changed files with 195 additions and 21 deletions
|
@ -2,6 +2,8 @@ import shutil
|
|||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from feedgen.feed import FeedGenerator
|
||||
|
||||
from zona import markdown as zmd
|
||||
from zona import util
|
||||
from zona.config import ZonaConfig
|
||||
|
@ -55,7 +57,9 @@ class ZonaBuilder:
|
|||
layout.root / "content" / "static"
|
||||
):
|
||||
logger.debug(f"Parsing {path.name}.")
|
||||
item.metadata, item.content = parse_metadata(path)
|
||||
item.metadata, item.content = parse_metadata(
|
||||
path, config=self.config
|
||||
)
|
||||
if item.metadata.ignore or (
|
||||
item.metadata.draft
|
||||
and not self.config.build.include_drafts
|
||||
|
@ -97,7 +101,45 @@ class ZonaBuilder:
|
|||
items.append(item)
|
||||
self.items = items
|
||||
|
||||
def _build(self):
|
||||
def generate_feed(self) -> bytes:
|
||||
post_list = self._get_post_list()
|
||||
config = self.config.feed
|
||||
if config.link.endswith("/"):
|
||||
config.link = config.link[:-2]
|
||||
fg = FeedGenerator()
|
||||
fg.id(config.link)
|
||||
fg.title(config.title)
|
||||
author = {
|
||||
"name": config.author.name,
|
||||
"email": config.author.email,
|
||||
}
|
||||
fg.author(author)
|
||||
fg.link(
|
||||
href=f"{config.link}/{config.path}",
|
||||
rel="self",
|
||||
type="application/rss+xml",
|
||||
)
|
||||
fg.language(config.language)
|
||||
fg.description(config.description)
|
||||
|
||||
for post in post_list:
|
||||
assert post.metadata
|
||||
fe = fg.add_entry() # pyright: ignore[reportUnknownVariableType]
|
||||
fe.id(f"{config.link}{util.normalize_url(post.url)}") # pyright: ignore[reportUnknownMemberType]
|
||||
fe.link( # pyright: ignore[reportUnknownMemberType]
|
||||
href=f"{config.link}{util.normalize_url(post.url)}"
|
||||
)
|
||||
fe.title(post.metadata.title) # pyright: ignore[reportUnknownMemberType]
|
||||
fe.author(author) # pyright: ignore[reportUnknownMemberType]
|
||||
desc = post.metadata.description
|
||||
fe.description(desc) # pyright: ignore[reportUnknownMemberType]
|
||||
date = post.metadata.date
|
||||
fe.pubDate(date) # pyright: ignore[reportUnknownMemberType]
|
||||
out: bytes = fg.rss_str(pretty=True) # pyright: ignore[reportUnknownVariableType]
|
||||
assert isinstance(out, bytes)
|
||||
return out
|
||||
|
||||
def _get_post_list(self) -> list[Item]:
|
||||
assert self.items
|
||||
# sort according to date
|
||||
# descending order
|
||||
|
@ -108,7 +150,12 @@ class ZonaBuilder:
|
|||
else date.min,
|
||||
reverse=True,
|
||||
)
|
||||
return post_list
|
||||
|
||||
def _build(self):
|
||||
post_list = self._get_post_list()
|
||||
# number of posts
|
||||
# generate RSS here
|
||||
posts = len(post_list)
|
||||
# link post chronology
|
||||
for i, item in enumerate(post_list):
|
||||
|
@ -180,4 +227,9 @@ class ZonaBuilder:
|
|||
self._discover()
|
||||
logger.debug("Building...")
|
||||
self._build()
|
||||
if self.config.feed.enabled:
|
||||
rss = self.generate_feed()
|
||||
path = self.layout.content / self.config.feed.path
|
||||
util.ensure_parents(path)
|
||||
path.write_bytes(rss)
|
||||
self.fresh = False
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, tzinfo
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import yaml
|
||||
from dacite import Config as DaciteConfig
|
||||
from dacite import from_dict
|
||||
|
||||
from zona.log import get_logger
|
||||
|
@ -25,9 +31,17 @@ def find_config(start: Path | None = None) -> Path | None:
|
|||
SitemapConfig = dict[str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PostDefaultsConfig:
|
||||
description: str = "A blog post"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlogConfig:
|
||||
dir: str = "blog"
|
||||
defaults: PostDefaultsConfig = field(
|
||||
default_factory=PostDefaultsConfig
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -69,12 +83,40 @@ class ServerConfig:
|
|||
reload: ReloadConfig = field(default_factory=ReloadConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthorConfig:
|
||||
name: str = "John Doe"
|
||||
email: str = "john@doe.net"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FeedConfig:
|
||||
enabled: bool = True
|
||||
timezone: tzinfo = field(default_factory=lambda: ZoneInfo("UTC"))
|
||||
path: str = "rss.xml"
|
||||
link: str = "https://example.com"
|
||||
title: str = "Zona Website"
|
||||
description: str = "My zona website."
|
||||
language: str = "en"
|
||||
author: AuthorConfig = field(default_factory=AuthorConfig)
|
||||
|
||||
|
||||
IGNORELIST = [".marksman.toml"]
|
||||
|
||||
|
||||
def parse_timezone(s: Any) -> tzinfo:
|
||||
if isinstance(s, str):
|
||||
return ZoneInfo(s)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected {str}, got {type(s)} for config key timezone"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZonaConfig:
|
||||
base_url: str = "/"
|
||||
feed: FeedConfig = field(default_factory=FeedConfig)
|
||||
# dictionary where key is name, value is url
|
||||
sitemap: SitemapConfig = field(
|
||||
default_factory=lambda: {"Home": "/"}
|
||||
|
@ -87,7 +129,12 @@ class ZonaConfig:
|
|||
server: ServerConfig = field(default_factory=ServerConfig)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "ZonaConfig":
|
||||
def from_file(cls, path: Path) -> ZonaConfig:
|
||||
with open(path, "r") as f:
|
||||
raw = yaml.safe_load(f)
|
||||
return from_dict(data_class=cls, data=raw)
|
||||
config: ZonaConfig = from_dict(
|
||||
data_class=cls,
|
||||
data=raw,
|
||||
config=DaciteConfig(type_hooks={tzinfo: parse_timezone}),
|
||||
)
|
||||
return config
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "base.html" %} {% block content %} {% if metadata.show_title %} {%
|
||||
include "title.html" %} {% if metadata.date %}
|
||||
include "title.html" %} {% if metadata.date.date() %}
|
||||
<center>
|
||||
<time class="post-date" datetime="{{ metadata.date | safe }}"
|
||||
>{{ metadata.date | safe}}</time>
|
||||
<time class="post-date" datetime="{{ metadata.date.date() | safe }}"
|
||||
>{{ metadata.date.date() | safe}}</time>
|
||||
</center>
|
||||
{% endif %} {% endif %} {% if is_post %} {% include "post_nav.html" %} {% endif
|
||||
%}
|
||||
|
|
|
@ -8,8 +8,8 @@ include "title.html" %} {% endif %}
|
|||
<ul>
|
||||
{% for item in post_list %}
|
||||
<li>
|
||||
<time class="post-list-date" datetime="{{ item.metadata.date | safe }}"
|
||||
>{{ item.metadata.date | safe}}</time>: <a href="/{{ item.url }}"
|
||||
<time class="post-list-date" datetime="{{ item.metadata.date.date() | safe }}"
|
||||
>{{ item.metadata.date.date() | safe}}</time>: <a href="/{{ item.url }}"
|
||||
>{{ item.metadata.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import typer
|
||||
import yaml
|
||||
|
@ -145,9 +146,13 @@ def initialize_site(root: Path | None = None):
|
|||
logger.debug("Loading default configuation.")
|
||||
config = ZonaConfig()
|
||||
logger.debug(f"Writing default configuration to {config_path}.")
|
||||
config_dict = asdict(config)
|
||||
if "feed" in config_dict and "timezone" in config_dict["feed"]:
|
||||
tz: ZoneInfo = config_dict["feed"]["timezone"]
|
||||
config_dict["feed"]["timezone"] = tz.key
|
||||
with open(config_path, "w") as f:
|
||||
yaml.dump(
|
||||
asdict(config),
|
||||
config_dict,
|
||||
f,
|
||||
sort_keys=False,
|
||||
default_flow_style=False,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from datetime import date, datetime, time, tzinfo
|
||||
from pathlib import Path
|
||||
|
||||
import frontmatter
|
||||
|
@ -10,13 +10,14 @@ from dateutil import parser as date_parser
|
|||
from yaml import YAMLError
|
||||
|
||||
import zona.util
|
||||
from zona.config import ZonaConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class Metadata:
|
||||
title: str
|
||||
date: date
|
||||
description: str | None
|
||||
date: datetime
|
||||
description: str
|
||||
show_title: bool = True
|
||||
show_date: bool = True
|
||||
show_nav: bool = True
|
||||
|
@ -30,14 +31,29 @@ class Metadata:
|
|||
math: bool = True
|
||||
|
||||
|
||||
def parse_date(raw_date: str | date | object) -> date:
|
||||
if isinstance(raw_date, date):
|
||||
return raw_date
|
||||
def ensure_timezone(dt: datetime, tz: tzinfo) -> datetime:
|
||||
if dt.tzinfo is None or dt.utcoffset() is None:
|
||||
dt = dt.replace(tzinfo=tz)
|
||||
return dt
|
||||
|
||||
|
||||
# TODO: migrate to using datetime, where user can optionall specify
|
||||
# a time as well. if only date is given, default to time.min
|
||||
def parse_date(
|
||||
raw_date: str | datetime | date | object, tz: tzinfo
|
||||
) -> datetime:
|
||||
if isinstance(raw_date, datetime):
|
||||
return ensure_timezone(raw_date, tz)
|
||||
elif isinstance(raw_date, date):
|
||||
return datetime.combine(raw_date, time.min, tzinfo=tz)
|
||||
assert isinstance(raw_date, str)
|
||||
return date_parser.parse(raw_date).date()
|
||||
dt = date_parser.parse(raw_date)
|
||||
return ensure_timezone(dt, tz)
|
||||
|
||||
|
||||
def parse_metadata(path: Path) -> tuple[Metadata, str]:
|
||||
def parse_metadata(
|
||||
path: Path, config: ZonaConfig
|
||||
) -> tuple[Metadata, str]:
|
||||
"""
|
||||
Parses a file and returns parsed Metadata and its content. Defaults
|
||||
are applied for missing fields. If there is no metadata, a Metadata
|
||||
|
@ -53,10 +69,11 @@ def parse_metadata(path: Path) -> tuple[Metadata, str]:
|
|||
raw_meta = post.metadata or {}
|
||||
defaults = {
|
||||
"title": zona.util.filename_to_title(path),
|
||||
"date": date.fromtimestamp(path.stat().st_ctime),
|
||||
"date": datetime.fromtimestamp(path.stat().st_ctime),
|
||||
"description": config.blog.defaults.description,
|
||||
}
|
||||
meta = {**defaults, **raw_meta}
|
||||
meta["date"] = parse_date(meta.get("date"))
|
||||
meta["date"] = parse_date(meta.get("date"), config.feed.timezone)
|
||||
try:
|
||||
metadata = from_dict(
|
||||
data_class=Metadata,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue