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