14bbd53b289ee0bd3ce2148d2b8797f1e7c35867
[books.alexwlchan.net] / scripts / render_html.py
1 #!/usr/bin/env python
2
3 import datetime
4 import itertools
5 import os
6 import pathlib
7 import re
8 import subprocess
9 import sys
10
11 import attr
12 import frontmatter
13 import markdown
14 from markdown.extensions.smarty import SmartyExtension
15 from jinja2 import Environment, FileSystemLoader, select_autoescape
16
17
18 def rsync(dir1, dir2):
19     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
20
21
22 @attr.s
23 class Book:
24     title = attr.ib()
25     author = attr.ib()
26     publication_year = attr.ib()
27     cover_image = attr.ib()
28     cover_desc = attr.ib(default="")
29
30     isbn10 = attr.ib(default="")
31     isbn13 = attr.ib(default="")
32
33
34 @attr.s
35 class Review:
36     date_read = attr.ib()
37     rating = attr.ib()
38     text = attr.ib()
39     format = attr.ib()
40     did_not_finish = attr.ib(default=False)
41
42
43 @attr.s
44 class ReviewEntry:
45     path = attr.ib()
46     book = attr.ib()
47     review = attr.ib()
48
49     def out_path(self):
50         name = self.path.with_suffix("").name
51         return pathlib.Path(f"reviews/{name}")
52
53
54 def get_review_entry_from_path(path):
55     post = frontmatter.load(path)
56
57     book = Book(**post["book"])
58     review = Review(**post["review"], text=post.content)
59
60     return ReviewEntry(path=path, book=book, review=review)
61
62
63 @attr.s
64 class CurrentlyReading:
65     text = attr.ib()
66
67
68 @attr.s
69 class CurrentlyReadingEntry:
70     path = attr.ib()
71     book = attr.ib()
72     reading = attr.ib()
73
74
75 def get_reading_entry_from_path(path):
76     post = frontmatter.load(path)
77
78     book = Book(**post["book"])
79     reading = CurrentlyReading(text=post.content)
80
81     return CurrentlyReadingEntry(path=path, book=book, reading=reading)
82
83
84 @attr.s
85 class Plan:
86     text = attr.ib()
87
88
89 @attr.s
90 class PlanEntry:
91     path = attr.ib()
92     book = attr.ib()
93     plan = attr.ib()
94
95
96 def get_plan_entry_from_path(path):
97     post = frontmatter.load(path)
98
99     book = Book(**post["book"])
100     plan = Plan(text=post.content)
101
102     return PlanEntry(path=path, book=book, plan=plan)
103
104
105 def get_entries(dirpath, constructor):
106     for dirpath, _, filenames in os.walk(dirpath):
107         for f in filenames:
108             if not f.endswith(".md"):
109                 continue
110
111             path = pathlib.Path(dirpath) / f
112
113             try:
114                 yield constructor(path)
115             except Exception:
116                 print(f"Error parsing {path}", file=sys.stderr)
117                 raise
118
119
120 def render_markdown(text):
121     return markdown.markdown(text, extensions=[SmartyExtension()])
122
123
124 def render_date(date_value):
125     if isinstance(date_value, datetime.date):
126         return date_value.strftime("%d %B %Y")
127
128     date_match = re.match(
129         r"^(?P<year>\d{4})-(?P<month>\d{2})(?:-(?P<day>\d{2}))?$", date_value
130     )
131     assert date_match is not None, date_value
132
133     date_obj = datetime.datetime(
134         year=int(date_match.group("year")),
135         month=int(date_match.group("month")),
136         day=int(date_match.group("day") or "1"),
137     )
138
139     if date_match.group("day"):
140         return date_obj.strftime("%-d %B %Y")
141     else:
142         return date_obj.strftime("%B %Y")
143
144
145 def render_individual_review(env, *, review_entry):
146     template = env.get_template("review.html")
147     html = template.render(
148         review_entry=review_entry, title=f"My review of {review_entry.book.title}"
149     )
150
151     out_name = review_entry.out_path() / "index.html"
152     out_path = pathlib.Path("_html") / out_name
153     out_path.parent.mkdir(exist_ok=True, parents=True)
154     out_path.write_text(html)
155
156
157 if __name__ == "__main__":
158     env = Environment(
159         loader=FileSystemLoader("templates"),
160         autoescape=select_autoescape(["html", "xml"]),
161     )
162
163     env.filters["render_markdown"] = render_markdown
164     env.filters["render_date"] = render_date
165
166     rsync("src/covers/", "_html/covers/")
167     rsync("static/", "_html/static/")
168
169     # Render the "all reviews page"
170
171     all_reviews = list(
172         get_entries(dirpath="src/reviews", constructor=get_review_entry_from_path)
173     )
174     all_reviews = sorted(
175         all_reviews, key=lambda rev: str(rev.review.date_read), reverse=True
176     )
177
178     for review_entry in all_reviews:
179         render_individual_review(env, review_entry=review_entry)
180
181     template = env.get_template("list_reviews.html")
182     html = template.render(
183         all_reviews=[
184             (year, list(reviews))
185             for (year, reviews) in itertools.groupby(
186                 all_reviews, key=lambda rev: str(rev.review.date_read)[:4]
187             )
188         ],
189         title="books i’ve read",
190         this_year=str(datetime.datetime.now().year),
191     )
192
193     out_path = pathlib.Path("_html") / "reviews/index.html"
194     out_path.write_text(html)
195
196     # Render the "currently reading" page
197
198     all_reading = list(
199         get_entries(
200             dirpath="src/currently_reading", constructor=get_reading_entry_from_path
201         )
202     )
203
204     template = env.get_template("list_reading.html")
205     html = template.render(all_reading=all_reading, title="books i’m currently reading")
206
207     out_path = pathlib.Path("_html") / "reading/index.html"
208     out_path.parent.mkdir(exist_ok=True, parents=True)
209     out_path.write_text(html)
210
211     # Render the "want to read" page
212
213     all_plans = list(
214         get_entries(dirpath="src/plans", constructor=get_plan_entry_from_path)
215     )
216
217     template = env.get_template("list_plans.html")
218     html = template.render(all_plans=all_plans, title="books i want to read")
219
220     out_path = pathlib.Path("_html") / "to-read/index.html"
221     out_path.parent.mkdir(exist_ok=True, parents=True)
222     out_path.write_text(html)
223
224     # Render the front page
225
226     index_template = env.get_template("index.html")
227     html = index_template.render(text=open("src/index.md").read())
228
229     index_path = pathlib.Path("_html") / "index.html"
230     index_path.write_text(html)
231
232     print("✨ Rendered HTML files to _html ✨")