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