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