]> git.za3k.com Git - blog.git/commitdiff
Add rss feed, authors, pagination magic
authorZachary Vance <za3k@za3k.com>
Wed, 10 Jul 2024 22:46:50 +0000 (18:46 -0400)
committerZachary Vance <za3k@za3k.com>
Wed, 10 Jul 2024 22:46:50 +0000 (18:46 -0400)
blog
config.yaml
templates/author.mustache.html [new file with mode: 0644]
templates/category.mustache.html
templates/deadlinks.mustache.html [new file with mode: 0644]
templates/feed.mustache.html [new file with mode: 0644]
templates/layout.mustache.html
templates/tag.mustache.html

diff --git a/blog b/blog
index e0ef4564d023012fef2a96dc07f763ed1e83566b..15db000bf7c9aad634172e5e9723857336331d42 100755 (executable)
--- a/blog
+++ b/blog
@@ -14,17 +14,18 @@ not documented.
 Other than that I think it's pretty nice!
 """
 
-# TODO: Pagination for main blog, categories, tag
-
 import sys
 sys.dont_write_bytecode = True
 
 import argparse
 import chevron as mustache
 import collections
+import datetime
 import math
 import collections
 import os, os.path
+import re
+import subprocess
 import yaml
 
 from pathlib import Path
@@ -32,6 +33,11 @@ from pathlib import Path
 import frontmatter
 import monitor
 
+NO_RESTART_EXIT_CODE = 27
+RESTART_EXIT_CODE = 28
+RFC822="%a, %d %b %Y %H:%M:%S %Z"
+FRONTMATTER_DT="%Y-%m-%d %H:%M:%S%:z"
+
 class PseudoMap():
     def __getitem__(self, key):
         try:
@@ -42,12 +48,41 @@ class PseudoMap():
         setattr(self, key, value)
 
 # I have verified wordpress slugs match this format too
-allowable="abcdefghijklmnopqrstuvwxyz0123456789-" 
+allowable="abcdefghijklmnopqrstuvwxyz0123456789-"
 def url_slug(title):
-    title = title.lower().replace(" ", "-")
+    title = title.lower().replace(" ", "-").replace(".", "-")
     title = "".join(x for x in title if x in allowable)
     return title
 
+def paginated_property(f):
+    # Add <PROPERTY>.pages and <PROPERTY>.first10 with deep python magic
+    class Paginated():
+        def __init__(self, lst):
+            self.lst = sorted(lst, key=lambda x: x.date, reverse=True)
+        def __iter__(self):
+            return iter(self.lst)
+        def __len__(self):
+            return len(self.lst)
+        @property
+        def pages(self, per_page=10):
+            for start in range(0, len(self.lst), per_page):
+                yield self.lst[start:start+per_page]
+        @property
+        def first10(self):
+            return self.lst[:10]
+        @property
+        def length(self):
+            return len(self.lst)
+    class AnonProperty():
+        def __init__(self, fget):
+            self.fget = fget
+        def __set_name__(self, owner, name):
+            self._name = "_" + name
+        def __get__(self, obj, objtype=None):
+            return Paginated(self.fget(obj))
+
+    return AnonProperty(f)
+
 def calc_range(l):
     it = iter(l)
     min = next(it)
@@ -84,6 +119,43 @@ setTimeout(function() {
 </script>
 """
 
+class Link():
+    def __init__(self, original, blog, source):
+        self.original = original
+        self.partial = original.replace("https://blog.za3k.com/","")
+        if self.partial.endswith("/"):
+            self.partial = self.partial.removesuffix("/")
+            if "/" not in self.partial:
+                self.partial = "posts/" + self.partial
+            self.partial += ".html"
+        self.blog = blog
+        self.source = source
+
+    @property
+    def wordpress(self):
+        return "https://blog.za3k.com/" + self.partial
+
+    @property
+    def static(self):
+        return "../" + self.partial
+
+    @property
+    def file(self):
+        return self.blog.destination + "/" + self.partial
+    
+    @property
+    def is_dead(self):
+        return not os.path.exists(self.file)
+
+    def __hash__(self):
+        return hash(self.partial)
+
+    def __lt__(self, other):
+        return self.partial < other.partial
+
+    def __eq__(self, other):
+        return self.partial == other.partial
+
 class Templatable(PseudoMap):
     use_layout = True
     def __init__(self, blog):
@@ -98,12 +170,17 @@ class Templatable(PseudoMap):
         output_path_template = self.blog["{}_destination".format(self.type)]
         return Path(mustache.render(output_path_template, self.context))
 
-    @staticmethod
-    def render_template(blog, name, context):
+    @property
+    def url(self):
+        return self.blog.web_root + "/" + self.output_path
+
+    def render_template(source, blog, name, context):
         template_path = blog["{}_template".format(name)]
         with open(template_path, "r") as f:
             template = f.read()
-        return mustache.render(template, context, warn=True)
+        html = mustache.render(template, context, warn=True)
+        blog.replace_links(source, html)
+        return html
 
     def content(self):
         content = self.render_template(self.blog, self.type, self.context)
@@ -112,7 +189,7 @@ class Templatable(PseudoMap):
                 "content": content,
             }, self, self.blog)).encode("utf8")
         else:
-            return
+            return content.encode("utf8")
 
     def output(self):
         output = self.content()
@@ -144,12 +221,17 @@ class Post(Templatable):
         self.post, self.comments = parsed.pop("content").split("<!-- comments -->\n")
         for k, v in parsed.items():
             self[k] = v
+
+    @property
+    def date_rfc822(self):
+        return self.date.strftime(RFC822)
+
     @property
     def id(self):
         if hasattr(self, "wordpress_slug"): return self.wordpress_slug
         if hasattr(self, "slug"): return self.slug
         return url_slug(self.title)
-
+        
     def __hash__(self):
         return hash(self.id)
 
@@ -158,14 +240,14 @@ class Tag(Templatable):
         super().__init__(blog)
         self.tag = tag
         self._posts = set()
-        self.slug = url_slug(tag)
+        self.slug = {"minecraft": "minecraft-2"}.get(tag, url_slug(tag))
 
     def add_post(self, post):
         self._posts.add(post)
 
-    @property
+    @paginated_property
     def posts(self):
-        return sorted(self._posts, key=lambda post: post.date, reverse=True)
+        return self._posts
 
     @property
     def num_posts(self):
@@ -178,7 +260,18 @@ class Category(Tag):
     pass
 
 class Page(Templatable):
-    pass # TODO
+    def __init__(self, page_name, blog, use_layout=None):
+        super().__init__(blog)
+        self.page_name = page_name
+        if use_layout is not None:
+            self.use_layout = use_layout
+
+    @property
+    def type(self):
+        return self.page_name
+
+class Author(Tag):
+    pass
 
 class Image(Templatable):
     use_layout = False
@@ -188,8 +281,12 @@ class Blog(PseudoMap):
     def __init__(self, config="config.yaml", reload=False):
         self.tags = {}
         self.categories = {}
-        self.posts = []
+        self.authors = {}
+        self._posts = []
         self.reload = reload
+        self.links = set()
+        self.now = datetime.datetime.now(datetime.timezone.utc)
+        self.now_rfc822 = self.now.strftime(RFC822)
 
         self.config = os.path.abspath(config)
         self.load_config(config)
@@ -206,17 +303,35 @@ class Blog(PseudoMap):
                 v = os.path.join(self.source, os.path.expanduser(v))
             self[k] = v
 
+    @property
+    def deadlinks(self):
+        return sorted(link for link in self.links if link.is_dead and all(x not in link.partial for x in ("?replytocom", "#comment")))
+
+    @paginated_property
+    def posts(self):
+        return self._posts
+
+    def replace_links(self, source, html):
+        link_regex = '(?<!srcset=")(?<=")https://blog.za3k.com/([^"]*)(?=")'
+        return re.sub(link_regex, lambda m: self.rewrite_link(source, m), html)
+
+    def rewrite_link(self, source, match):
+        link = Link(match.group(0), self, source.output_path)
+        self.links.add(link)
+        return link.static
+
     def load_posts(self):
         for post_input_path in Path(self.post_dir).iterdir():
             self.add_post(Post(frontmatter.load(post_input_path), self))
 
     def add_post(self, post):
-        self.posts.append(post)
+        self._posts.append(post)
 
         for tag in post.tags:
             self.tag_for(tag).add_post(post)
         for category in post.categories:
             self.category_for(category).add_post(post)
+        self.author_for(post.author).add_post(post)
 
     def category_for(self, category):
         if category not in self.categories:
@@ -228,6 +343,11 @@ class Blog(PseudoMap):
             self.tags[tag] = Tag(tag, self)
         return self.tags[tag]
 
+    def author_for(self, author):
+        if author not in self.authors:
+            self.authors[author] = Author(author, self)
+        return self.authors[author]
+
     @property
     def static(self):
         for dirpath, dirnames, filenames in Path(self.static_dir).walk():
@@ -237,7 +357,10 @@ class Blog(PseudoMap):
 
     @property
     def pages(self):
-        return [] # TODO
+        return [
+            Page("feed", self, use_layout=False),
+            Page("deadlinks", self), # Must be last to avoid dead links
+        ]
 
     @property
     def images(self):
@@ -254,6 +377,8 @@ class Blog(PseudoMap):
             tag.output()
         for category in blog.categories.values():
             category.output()
+        for author in blog.authors.values():
+            author.output()
         for page in blog.pages:
             page.output()
 
@@ -263,7 +388,9 @@ class Blog(PseudoMap):
 
     @staticmethod
     def reboot():
-        os.execl(sys.argv[0], *sys.argv)
+        #os.execl(sys.argv[0], *sys.argv)
+        sys.exit(RESTART_EXIT_CODE)
+     
 
     @property
     def tagcloud(self, font_sizes=range(8, 22), limit=45):
@@ -279,7 +406,7 @@ class Blog(PseudoMap):
         for tag in top_tags:
             tag.font_size = scale(tag_scaling(tag.num_posts), post_count_range, font_sizes)
         
-        return Templatable.render_template(blog, "tagcloud", self)
+        return Templatable.render_template(Templatable, blog, "tagcloud", self)
 
     def _update_happened(self, path):
         path = Path(path)
@@ -320,7 +447,17 @@ class Blog(PseudoMap):
     def publish(self):
         os.system("rsync -r --delete {destination}/ germinate:/var/www/blog".format(destination=self.destination))
 
+
+def supervisor():
+    while True:
+        result = subprocess.run([sys.argv[0], "--supervised"] + sys.argv[1:])
+        if result.returncode == NO_RESTART_EXIT_CODE:
+            break
+
 if __name__ == "__main__":
+    if "--supervised" not in sys.argv:
+        supervisor()
+
     parser = argparse.ArgumentParser(
         prog="blog",
         description="Generate za3k's blog from HTML/markdown files with YAML frontmatter and some templates",
@@ -330,8 +467,10 @@ if __name__ == "__main__":
     parser.add_argument("-f", "--follow", action='store_true', help="continue running and monitoring for file changes")
     parser.add_argument("-l", "--local", action='store_true', help="use relative paths for links")
     parser.add_argument("-r", "--reload", action='store_true', help="reload the page automatically")
+    parser.add_argument("--supervised", action='store_true')
     args = parser.parse_args()
 
+    assert args.supervised
     if len(args.changed_files) == 0:
         args.all = True
 
@@ -348,5 +487,9 @@ if __name__ == "__main__":
     if args.follow:
         print("monitoring for changes...", file=sys.stderr)
         # Discard updates within 5s of one another
-        for changed_file in monitor.Monitor(blog.source, discard_rapid=5): 
-            blog.updates_happened([changed_file])
+        try:
+            for changed_file in monitor.Monitor(blog.source, discard_rapid=5): 
+                blog.updates_happened([changed_file])
+        except KeyboardInterrupt:
+            pass
+    sys.exit(NO_RESTART_EXIT_CODE)
index 806e4aedd973d3ee40bd4af94fb45409d0fd4db2..e06481d17a7397319f9e84e53184aa54b8a8f26e 100644 (file)
@@ -8,15 +8,22 @@ post_dir: "posts"
 page_dir: "pages"
 image_dir: "images"
 static_dir: "static"
-tag_template: "templates/tag.mustache.html"
+
+author_template: "templates/author.mustache.html"
 category_template: "templates/tag.mustache.html"
+deadlinks_template: "templates/deadlinks.mustache.html"
+feed_template: "templates/feed.mustache.html"
+layout_template: "templates/layout.mustache.html"
 post_template: "templates/post.mustache.html"
+tag_template: "templates/tag.mustache.html"
 tagcloud_template: "templates/tagcloud.mustache.html"
-layout_template: "templates/layout.mustache.html"
 
-post_destination: "{{destination}}/posts/{{id}}.html"
-tag_destination: "{{destination}}/tag/{{slug}}.html"
+author_destination: "{{destination}}/author/{{slug}}.html"
 category_destination: "{{destination}}/category/{{slug}}.html"
-page_destination: "{{destination}}/{{page}}"
+deadlinks_destination: "{{destination}}/pages/deadlinks.html"
+feed_destination: "{{destination}}/pages/feed.xml"
 image_destination: "{{destination}}/images/{{image}}"
+page_destination: "{{destination}}/{{page}}"
+post_destination: "{{destination}}/posts/{{id}}.html"
 static_destination: "{{destination}}/{{relative_path}}"
+tag_destination: "{{destination}}/tag/{{slug}}.html"
diff --git a/templates/author.mustache.html b/templates/author.mustache.html
new file mode 100644 (file)
index 0000000..ec6c322
--- /dev/null
@@ -0,0 +1,7 @@
+<header class="page-header">
+    <h1 class="page-title author">Author Archives: <span class="vcard"><a class="url fn n" href="https://blog.za3k.com/author/{{ slug }}/" title="admin" rel="me">{{ tag }}</a></span></h1>
+</header>
+
+{{# posts.first10 }}
+    {{& post }}
+{{/ posts.first10 }}
index 830c2cf06bd9dd5e4726b805e3271d4282b4afe7..f55865867f6db7c467c11d2d7a16c49d0784f5ca 100644 (file)
@@ -2,6 +2,6 @@
     <h1 class="page-title">Category Archives: <span>{{ tag }}</span></h1>
 </header>
 
-{{# posts }}
+{{# posts.first10 }}
     {{& post }}
-{{/ posts }}
+{{/ posts.first10 }}
diff --git a/templates/deadlinks.mustache.html b/templates/deadlinks.mustache.html
new file mode 100644 (file)
index 0000000..3003ce4
--- /dev/null
@@ -0,0 +1,5 @@
+<ol>
+{{# deadlinks }}
+<li><a href="{{static}}">{{partial}}</a> <a href="{{wordpress}}">[orig]</a> <a href="{{source}}">[src]</a>
+{{/ deadlinks }}
+</ol>
diff --git a/templates/feed.mustache.html b/templates/feed.mustache.html
new file mode 100644 (file)
index 0000000..7406b84
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0"
+       xmlns:content="http://purl.org/rss/1.0/modules/content/"
+       xmlns:wfw="http://wellformedweb.org/CommentAPI/"
+       xmlns:dc="http://purl.org/dc/elements/1.1/"
+       xmlns:atom="http://www.w3.org/2005/Atom"
+       xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+       xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
+       >
+<channel>
+       <title>{{title}}</title>
+       <atom:link href="https://blog.za3k.com/pages/feed.xml" rel="self" type="application/rss+xml" />
+       <link>{{web_root}}</link>
+       <description>{{title}}</description>
+       <lastBuildDate>{{now_rfc822}}</lastBuildDate>
+       <language>en-US</language>
+       <sy:updatePeriod>hourly</sy:updatePeriod>
+       <sy:updateFrequency>1</sy:updateFrequency>
+    {{# posts.first10 }}
+       <item>
+               <title>{{title}}</title>
+               <link>{{web_root}}/post/{{id}}.html</link>
+               <dc:creator><![CDATA[{{author}}]]></dc:creator>
+               <pubDate>{{date_rfc822}}</pubDate>
+        {{# categories }}
+        <category><![CDATA[{{.}}]]></category>
+        {{/ categories }}
+        {{# tags }}
+        <category><![CDATA[{{.}}]]></category>
+        {{/ tags }}
+               <guid isPermaLink="false">{{id}}</guid>
+        <!--<description><![CDATA[SHORT_DESCRIPTION]]></description>-->
+        <content:encoded><![CDATA[{{post}}]]></content:encoded>
+    </item>
+    {{/ posts.first10 }}
+</channel>
+</rss>
index e988917ea317bd49eadd8c175cec109995f3923a..97be356c16a3e4aed42a3774d26f2803fecf8953 100644 (file)
@@ -4,9 +4,8 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
     <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0, minimum-scale=1.0, maximum-scale=3.0">
     <link rel="profile" href="http://gmpg.org/xfn/11">
-    <link rel="pingback" href="https://blog.za3k.com/xmlrpc.php">
     <meta name="robots" content="max-image-preview:large">
-    <link rel="alternate" type="application/rss+xml" title=" » Feed" href="https://blog.za3k.com/feed/">
+    <link rel="alternate" type="application/rss+xml" title=" » Feed" href="https://blog.za3k.com/pages/feed.xml">
     <style id="wp-emoji-styles-inline-css" type="text/css">
         img.wp-smiley, img.emoji {
             display: inline !important;
@@ -57,7 +56,7 @@
                     <div id="site-description">blog of zachary "za3k" vance</div>
                 </div>
                 <div class="socials" id="sheader">
-                    <a target="_blank" rel="nofollow" href="https://blog.za3k.com/feed/" class="socialicons social-RSS" title="RSS">
+                    <a target="_blank" rel="nofollow" href="https://blog.za3k.com/pages/feed.xml" class="socialicons social-RSS" title="RSS">
                         <img alt="RSS" src="../wp-content/themes/mantra/resources/images/socials/RSS.png">
                     </a>
                     <a target="_blank" rel="nofollow" href="https://twitter.com/unchoke" class="socialicons social-Twitter" title="Twitter">
index 056b87c011d5050d4be5ca03f5312b43920fd0b9..313419ff739f526038dacdde3313dd2db886c5eb 100644 (file)
@@ -2,6 +2,6 @@
     <h1 class="page-title">Tag Archives: <span>{{ tag }}</span></h1>
 </header>
 
-{{# posts }}
+{{# posts.first10 }}
     {{& post }}
-{{/ posts }}
+{{/ posts.first10 }}