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