evennia/devblog/_src/build_devblog.py

262 lines
7 KiB
Python
Raw Normal View History

"""
A simple tool for building the blog from markdown files. Uses Jinja2 for templating and
mistletoe for markdown->html generation.
Each blog file should be named `YYYY-MM-DD-Name-of-Post.md` It can have a header
with meta info:
```
title: Title in free form
date: Date in free form (if different from current date)
2021-11-14 23:04:10 +01:00
copyrights: Image references/links in markdown format (all on one line)
---
Here starts the main text ...
```
"""
import glob
from dataclasses import dataclass
from collections import defaultdict
2021-11-14 23:04:10 +01:00
from dateutil import parser as dateparser
from datetime import datetime
2021-11-16 01:47:08 +01:00
from os import symlink, remove, chdir
from os.path import abspath, dirname, join as pathjoin, sep
import jinja2
2021-11-16 01:47:08 +01:00
import mistletoe
from mistletoe import HTMLRenderer
from pygments import highlight
from pygments.styles import get_style_by_name as get_style
from pygments.lexers import get_lexer_by_name as get_lexer, guess_lexer
from pygments.formatters.html import HtmlFormatter
2021-11-14 23:04:10 +01:00
CURRDIR = dirname(abspath(__file__))
2021-11-16 00:12:06 +01:00
SOURCE_DIR = pathjoin(CURRDIR, "markdown")
2021-11-14 23:04:10 +01:00
TEMPLATE_DIR = pathjoin(CURRDIR, "templates")
2021-11-16 00:42:26 +01:00
IMG_DIR_NAME = "images"
IMG_DIR = pathjoin(CURRDIR, "images")
IMG_REL_LINK = "_src/images"
2021-11-16 00:12:06 +01:00
OUTDIR = dirname(CURRDIR)
2021-11-16 00:42:26 +01:00
OUTFILE_TEMPLATE = "{year}.html"
OUT_IMG_DIR = "images"
2021-11-14 23:04:10 +01:00
START_PAGE = "index.html"
2021-11-16 00:42:26 +01:00
BLOG_TEMPLATE = "blog.html"
POST_TEMPLATE = "post.html"
2021-11-14 23:04:10 +01:00
CURRENT_YEAR = datetime.now().year
2021-11-14 16:04:05 +01:00
@dataclass
2021-11-15 23:51:01 +01:00
class BlogPost:
"""
A single blog post.
"""
title: str
2021-11-15 23:51:01 +01:00
blurb: str
2021-11-14 23:04:10 +01:00
permalink: str
2021-11-15 23:51:01 +01:00
pagelink: str
2021-11-14 23:04:10 +01:00
anchor: str
date_pretty: str
date_short: str
date_sort: int
image_copyrights: str
html: str
2021-11-15 23:51:01 +01:00
@dataclass
class BlogPage:
"""
Represents one year/html page of blog posts.
"""
year: int
permalink: str
posts: list
calendar: dict
2021-11-16 01:47:08 +01:00
class PygmentsRenderer(HTMLRenderer):
"""
Custom syntax highlighter for misteltoe (based on
https://github.com/miyuchina/mistletoe/blob/master/contrib/pygments_renderer.py)
"""
formatter = HtmlFormatter()
formatter.noclasses = True
def __init__(self, *extras, style='default'):
super().__init__(*extras)
self.formatter.style = get_style(style)
def render_block_code(self, token):
code = token.children[0].content
lexer = get_lexer(token.language) if token.language else guess_lexer(code)
return highlight(code, lexer, self.formatter)
def md2html():
2021-11-14 23:04:10 +01:00
"""
Generate all blog pages, with one page per year.
"""
jinja_templates = jinja2.FileSystemLoader(TEMPLATE_DIR)
jinja_env = jinja2.Environment(loader=jinja_templates)
blog_template = jinja_env.get_template(BLOG_TEMPLATE)
post_template = jinja_env.get_template(POST_TEMPLATE)
2021-11-14 16:04:05 +01:00
calendar = defaultdict(list)
for file_path in glob.glob(pathjoin(SOURCE_DIR, "*.md")):
# parse/check if file is on form YY-MM-DD-Blog-Name.md
filename = file_path.rsplit(sep, 1)[1] if sep in file_path else file_path
try:
year, month, day, title = filename.split("-", 3)
except ValueError:
print(f"Warning: Markdown file '{filename}' is not on a recognized format. Skipping.")
continue
title = title[:-3] # remove .md ending
2021-11-15 23:51:01 +01:00
blurb = title[:11] + "..."
2021-11-15 01:24:37 +01:00
title = " ".join(title.split("-"))
2021-11-14 23:04:10 +01:00
date = datetime(year=int(year), month=int(month), day=int(day))
image_copyrights = ""
with open(file_path) as fil:
lines = fil.readlines()
2021-11-15 23:51:01 +01:00
# check if there is a meta header in the first 6 lines
meta = None
try:
meta_end = [lin.strip() for lin in lines[:5]].index("---")
except ValueError:
pass
else:
meta = lines[:meta_end]
2021-11-14 23:04:10 +01:00
lines = lines[meta_end + 1:]
for line in meta:
line = line.strip()
if line.startswith("title:"):
title = line.strip()[6:]
elif line.startswith("date:"):
2021-11-14 23:04:10 +01:00
date = dateparser.parse(line[5:])
elif line.startswith("copyrights:"):
image_copyrights = line[12:]
image_copyrights = mistletoe.markdown(image_copyrights)
2021-11-15 23:51:01 +01:00
elif line.startswith("blurb:"):
blurb = line[6:].strip()
markdown_post = "\n".join(lines)
# convert markdown to html
2021-11-16 01:47:08 +01:00
html = mistletoe.markdown(markdown_post, PygmentsRenderer)
2021-11-14 23:04:10 +01:00
# build the permalink
anchor = "{}".format(
date.strftime("%Y-%m-%d-" + "-".join(title.strip().lower().split()))
)
2021-11-15 23:51:01 +01:00
pagelink = OUTFILE_TEMPLATE.format(year=date.year)
permalink = "{}#{}".format(pagelink, anchor)
blogpost = BlogPost(
2021-11-14 23:04:10 +01:00
title=title,
2021-11-15 23:51:01 +01:00
blurb=blurb,
2021-11-14 23:04:10 +01:00
permalink=permalink,
2021-11-15 23:51:01 +01:00
pagelink=pagelink,
2021-11-14 23:04:10 +01:00
anchor=anchor,
date_pretty=date.strftime("%B %e, %Y"),
2021-11-15 23:51:01 +01:00
date_short=date.strftime("%b %e"),
2021-11-14 23:04:10 +01:00
date_sort=date.toordinal(),
image_copyrights=image_copyrights,
html=html
)
2021-11-15 23:51:01 +01:00
# populate template with jinja and readd to blogpost
context = {
2021-11-15 23:51:01 +01:00
"blogpost": blogpost
}
2021-11-15 23:51:01 +01:00
blogpost.html = post_template.render(context)
2021-11-14 16:04:05 +01:00
# store
2021-11-15 23:51:01 +01:00
calendar[date.year].append(blogpost)
2021-11-14 23:04:10 +01:00
# make sure to sort all entries by date
2021-11-15 23:51:01 +01:00
blogpages = []
for year in sorted(calendar, reverse=True):
blogpages.append(
BlogPage(
year=year,
permalink=OUTFILE_TEMPLATE.format(year=year),
posts=list(sorted(calendar[year], key=lambda post: -post.date_sort)),
calendar=calendar
)
)
2021-11-14 23:04:10 +01:00
2021-11-15 23:51:01 +01:00
# build the blog pages, per year
html_pages = {}
for blogpage in blogpages:
2021-11-16 00:42:26 +01:00
print(f"Processing blogs from {blogpage.year} ...")
2021-11-14 23:04:10 +01:00
context = {
2021-11-15 23:51:01 +01:00
"pageyear": blogpage.year,
"blogpage": blogpage,
"blogpages": blogpages
2021-11-14 23:04:10 +01:00
}
html_page = blog_template.render(context)
2021-11-15 23:51:01 +01:00
html_pages[blogpage.year] = html_page
2021-11-14 23:04:10 +01:00
2021-11-15 23:51:01 +01:00
return html_pages
2021-11-14 23:04:10 +01:00
def build_pages(blog_pages):
"""
Generate devblog pages.
"""
2021-11-16 00:42:26 +01:00
2021-11-16 00:12:06 +01:00
for html_file in glob.glob(pathjoin(OUTDIR, "*.html")):
remove(html_file)
2021-11-16 00:42:26 +01:00
try:
remove(pathjoin(OUTDIR, START_PAGE))
except FileNotFoundError:
pass
try:
remove(pathjoin(OUTDIR, IMG_DIR_NAME))
except FileNotFoundError:
pass
2021-11-16 00:12:06 +01:00
2021-11-14 23:04:10 +01:00
html_pages = md2html()
2021-11-14 16:04:05 +01:00
2021-11-14 23:04:10 +01:00
latest_year = -1
latest_page = None
for year, html_page in html_pages.items():
filename = pathjoin(OUTDIR, OUTFILE_TEMPLATE.format(year=year))
if year > latest_year:
latest_year = year
2021-11-16 00:42:26 +01:00
latest_page = filename.rsplit(sep, 1)[1] if sep in filename else filename
2021-11-14 23:04:10 +01:00
with open(filename, 'w') as fil:
fil.write(html_page)
2021-11-16 00:42:26 +01:00
chdir(OUTDIR)
symlink(IMG_REL_LINK, IMG_DIR_NAME)
symlink(latest_page, START_PAGE)
2021-11-16 00:42:26 +01:00
print(f"Output htmls written to {OUTDIR}{sep}. Latest year is {latest_year}.")
2021-11-14 16:04:05 +01:00
if __name__ == "__main__":
2021-11-14 23:04:10 +01:00
pages = md2html()
build_pages(pages)