Allow specifying an editor as well as/instead of an author
[books.alexwlchan.net] / scripts / render_html.py
index f241441..048a7fc 100755 (executable)
@@ -10,8 +10,9 @@ import subprocess
 import sys
 
 import attr
 import sys
 
 import attr
+import bs4
+import cssmin
 import frontmatter
 import frontmatter
-import htmlmin
 from jinja2 import Environment, FileSystemLoader, select_autoescape
 import markdown
 from markdown.extensions.smarty import SmartyExtension
 from jinja2 import Environment, FileSystemLoader, select_autoescape
 import markdown
 from markdown.extensions.smarty import SmartyExtension
@@ -25,11 +26,55 @@ def rsync(dir1, dir2):
     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
 
 
     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
 
 
+def git(*args):
+    return subprocess.check_output(["git"] + list(args)).strip().decode("utf8")
+
+
+def set_git_timestamps():
+    """
+    For everything in the covers/ directory, set the last modified timestamp to
+    the last time it was modified in Git.  This should make tint colour computations
+    stable across machines.
+    """
+    root = git("rev-parse", "--show-toplevel")
+
+    now = datetime.datetime.now().timestamp()
+
+    for f in os.listdir("src/covers"):
+        path = os.path.join("src/covers", f)
+
+        if not os.path.isfile(path):
+            continue
+
+        stat = os.stat(path)
+
+        # If the modified time is >7 days ago, skip setting the modified time.  This means
+        # the script stays pretty fast when doing a regular sync.
+        if now - stat.st_mtime > 7 * 24 * 60 * 60 and "--reset" not in sys.argv:
+            continue
+
+        revision = git("rev-list", "--max-count=1", "HEAD", path)
+
+        if not revision:
+            continue
+
+        timestamp, *_ = git("show", "--pretty=format:%ai", "--abbrev-commit", revision).splitlines()
+        modified_time = datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S %z").timestamp()
+
+        access_time = stat.st_atime
+
+        os.utime(path, times=(access_time, modified_time))
+
+
 @attr.s
 class Book:
 @attr.s
 class Book:
+    slug = attr.ib()
     title = attr.ib()
     title = attr.ib()
-    author = attr.ib()
     publication_year = attr.ib()
     publication_year = attr.ib()
+
+    author = attr.ib(default="")
+    editor = attr.ib(default="")
+
     cover_image = attr.ib(default="")
     cover_desc = attr.ib(default="")
 
     cover_image = attr.ib(default="")
     cover_desc = attr.ib(default="")
 
@@ -41,6 +86,7 @@ class Book:
 class Review:
     date_read = attr.ib()
     text = attr.ib()
 class Review:
     date_read = attr.ib()
     text = attr.ib()
+    date_order = attr.ib(default=1)
     format = attr.ib(default=None)
     rating = attr.ib(default=None)
     did_not_finish = attr.ib(default=False)
     format = attr.ib(default=None)
     rating = attr.ib(default=None)
     did_not_finish = attr.ib(default=False)
@@ -67,6 +113,7 @@ def get_review_entry_from_path(path):
         except KeyError:
             pass
 
         except KeyError:
             pass
 
+    kwargs["slug"] = os.path.basename(os.path.splitext(path)[0])
     book = Book(**kwargs)
 
     review = Review(**post["review"], text=post.content)
     book = Book(**kwargs)
 
     review = Review(**post["review"], text=post.content)
@@ -89,7 +136,9 @@ class CurrentlyReadingEntry:
 def get_reading_entry_from_path(path):
     post = frontmatter.load(path)
 
 def get_reading_entry_from_path(path):
     post = frontmatter.load(path)
 
-    book = Book(**post["book"])
+    slug = os.path.basename(os.path.splitext(path)[0])
+    book = Book(slug=slug, **post["book"])
+
     reading = CurrentlyReading(text=post.content)
 
     return CurrentlyReadingEntry(path=path, book=book, reading=reading)
     reading = CurrentlyReading(text=post.content)
 
     return CurrentlyReadingEntry(path=path, book=book, reading=reading)
@@ -118,7 +167,9 @@ class PlanEntry:
 def get_plan_entry_from_path(path):
     post = frontmatter.load(path)
 
 def get_plan_entry_from_path(path):
     post = frontmatter.load(path)
 
-    book = Book(**post["book"])
+    slug = os.path.basename(os.path.splitext(path)[0])
+    book = Book(slug=slug, **post["book"])
+
     plan = Plan(date_added=post["plan"]["date_added"], text=post.content)
 
     return PlanEntry(path=path, book=book, plan=plan)
     plan = Plan(date_added=post["plan"]["date_added"], text=post.content)
 
     return PlanEntry(path=path, book=book, plan=plan)
@@ -168,7 +219,18 @@ def save_html(template, out_name="", **kwargs):
     html = template.render(**kwargs)
     out_path = pathlib.Path("_html") / out_name / "index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
     html = template.render(**kwargs)
     out_path = pathlib.Path("_html") / out_name / "index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
-    out_path.write_text(htmlmin.minify(html))
+
+    soup = bs4.BeautifulSoup(html, "html.parser")
+
+    # Minify the CSS in all inline <style> tags.
+    for style_tag in soup.find_all("style"):
+        style_tag.string = cssmin.cssmin(style_tag.string)
+
+    # Remove any comments
+    for comment in soup(text=lambda text: isinstance(text, bs4.Comment)):
+        comment.extract()
+
+    out_path.write_text(str(soup))
 
 
 def _create_new_thumbnail(src_path, dst_path):
 
 
 def _create_new_thumbnail(src_path, dst_path):
@@ -225,6 +287,8 @@ def css_hash(_):
 
 
 def main():
 
 
 def main():
+    set_git_timestamps()
+
     env = Environment(
         loader=FileSystemLoader("templates"),
         autoescape=select_autoescape(["html", "xml"]),
     env = Environment(
         loader=FileSystemLoader("templates"),
         autoescape=select_autoescape(["html", "xml"]),
@@ -250,7 +314,7 @@ def main():
         get_entries(dirpath="src/reviews", constructor=get_review_entry_from_path)
     )
     all_reviews = sorted(
         get_entries(dirpath="src/reviews", constructor=get_review_entry_from_path)
     )
     all_reviews = sorted(
-        all_reviews, key=lambda rev: str(rev.review.date_read), reverse=True
+        all_reviews, key=lambda rev: f"{rev.review.date_read}/{rev.review.date_order}", reverse=True
     )
 
     for review_entry in all_reviews:
     )
 
     for review_entry in all_reviews: