Strip the leading zero when displaying dates
[books.alexwlchan.net] / scripts / render_html.py
1 #!/usr/bin/env python
2
3 import datetime
4 import os
5 import pathlib
6 import re
7 import subprocess
8 import sys
9
10 import attr
11 import frontmatter
12 import markdown
13 from markdown.extensions.smarty import SmartyExtension
14 from jinja2 import Environment, FileSystemLoader, select_autoescape
15
16
17 def rsync(dir1, dir2):
18     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
19
20
21 @attr.s
22 class Book:
23     title = attr.ib()
24     author = attr.ib()
25     publication_year = attr.ib()
26     cover_image = attr.ib()
27     cover_desc = attr.ib(default="")
28
29     isbn_13 = attr.ib(default="")
30
31
32 @attr.s
33 class Review:
34     date_read = attr.ib()
35     rating = attr.ib()
36     text = attr.ib()
37
38
39 @attr.s
40 class ReviewEntry:
41     path = attr.ib()
42     book = attr.ib()
43     review = attr.ib()
44
45     def out_path(self):
46         return self.path.relative_to("src").with_suffix("")
47
48
49 def get_review_entry_from_path(path):
50     post = frontmatter.load(path)
51
52     book = Book(**post["book"])
53     review = Review(**post["review"], text=post.content)
54
55     return ReviewEntry(path=path, book=book, review=review)
56
57
58 def get_reviews():
59     for dirpath, _, filenames in os.walk("src/reviews"):
60         for f in filenames:
61             if not f.endswith(".md"):
62                 continue
63
64             path = pathlib.Path(dirpath) / f
65
66             try:
67                 yield get_review_entry_from_path(path)
68             except Exception:
69                 print(f"Error parsing {path}", file=sys.stderr)
70                 raise
71
72
73 def render_markdown(text):
74     return markdown.markdown(text, extensions=[SmartyExtension()])
75
76
77 def render_date(date_value):
78     if isinstance(date_value, datetime.date):
79         return date_value.strftime("%d %B %Y")
80
81     date_match = re.match(
82         r"^(?P<year>\d{4})-(?P<month>\d{2})(?:-(?P<day>\d{2}))?$", date_value
83     )
84     assert date_match is not None, date_value
85
86     date_obj = datetime.datetime(
87         year=int(date_match.group("year")),
88         month=int(date_match.group("month")),
89         day=int(date_match.group("day") or "1"),
90     )
91
92     if date_match.group("day"):
93         return date_obj.strftime("%-d %B %Y")
94     else:
95         return date_obj.strftime("%B %Y")
96
97
98 def render_individual_review(env, *, review_entry):
99     template = env.get_template("review.html")
100     html = template.render(review_entry=review_entry)
101
102     out_name = review_entry.out_path() / "index.html"
103     out_path = pathlib.Path("_html") / out_name
104     out_path.parent.mkdir(exist_ok=True, parents=True)
105     out_path.write_text(html)
106
107
108 if __name__ == "__main__":
109     all_reviews = list(get_reviews())
110     all_reviews = sorted(
111         all_reviews, key=lambda rev: str(rev.review.date_read), reverse=True
112     )
113
114     env = Environment(
115         loader=FileSystemLoader("templates"),
116         autoescape=select_autoescape(["html", "xml"]),
117     )
118
119     env.filters["render_markdown"] = render_markdown
120     env.filters["render_date"] = render_date
121
122     rsync("src/covers/", "_html/covers/")
123     rsync("src/static/", "_html/static/")
124
125     for review_entry in all_reviews:
126         render_individual_review(env, review_entry=review_entry)
127
128     template = env.get_template("list_reviews.html")
129     html = template.render(all_reviews=all_reviews)
130
131     out_path = pathlib.Path("_html") / "reviews/index.html"
132     out_path.write_text(html)