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