This website is generated from Markdown source files using the below script. My requirements are minimal (no blog archive pages or anything) so writing my own short script was a better choice than using an existing, overcomplicated static site generator.
Generates a .html
file of the same name next to every .md
file in the given directories.
#!/usr/bin/env python3
import glob, os, os.path, re, sys
from os.path import getmtime, isfile
from mako.template import Template
import markdown
import markdown.treeprocessors
import markdown.extensions.toc
class TitleExtension(markdown.Extension):
"""Sets md.title to the first heading in the input file"""
class Treeprocessor(markdown.treeprocessors.Treeprocessor):
def __init__(self, md):
super().__init__(md)
self.header_rgx = re.compile(r'[Hh][123456]')
def run(self, doc):
for el in doc.iter():
if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
self.md.title = markdown.extensions.toc.get_name(el)
return
def extendMarkdown(self, md):
self.md = md
md.registerExtension(self)
proc = self.Treeprocessor(md)
md.treeprocessors.register(proc, 'title', 5)
def reset(self):
self.md.title = None
md = markdown.Markdown(
extensions=['abbr', 'smarty', 'sane_lists', 'tables',
'toc', TitleExtension()],
extension_configs={
'toc': {
'marker': '{TOC}',
'title': 'Contents',
'toc_depth': '2-6'
}
})
def render(srcname, dstname, template):
md.reset()
with open(srcname, 'r') as src, open(dstname, 'w') as dst:
content = md.convert(src.read())
dst.write(template.render(
title=md.title or 'Untitled',
content=content,
has_table='<table' in content,
has_form='<form' in content,
has_toc='<div class="toc' in content
))
def find_updated(dirs, template_mtime, verbose):
updated = []
for d in dirs:
pattern = os.path.join(glob.escape(d), '**/*.md')
for srcname in glob.glob(pattern, recursive=True):
dstname = srcname[:-3]+'.html'
if not isfile(dstname) \
or getmtime(srcname) > getmtime(dstname) \
or template_mtime > getmtime(dstname):
updated.append((srcname, dstname))
else:
verbose and print('Up to date:', dstname)
return updated
def adjacent_to_script(f):
base = os.path.dirname(os.path.realpath(__file__))
return os.path.join(base, f)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Static site generator')
parser.add_argument('dirs', metavar='dir', nargs='*',
help='directories to process (default: public/ draft/)',
default=[adjacent_to_script(d) for d in ('public', 'draft')])
parser.add_argument('--template',
default=adjacent_to_script('template.html'),
help='Mako template to use (default: template.html)')
parser.add_argument('-f', '--force', action='store_true',
help='overwrite files without prompting')
parser.add_argument('-v', '--verbose', action='store_true',
help='print status of every processed file')
args = parser.parse_args()
updated = find_updated(args.dirs, getmtime(args.template), args.verbose)
if updated:
overwrite = [dst for src, dst in updated if isfile(dst)]
if overwrite and not args.force:
print('These files will be overwritten:')
for dst in overwrite:
print(' '+dst)
response = input('Continue? [no] ')
if len(response) < 1 or response[0].lower() != 'y':
sys.exit(1)
template = Template(filename=args.template)
for src, dst in updated:
if args.verbose:
print('{}: {}'.format(
'Overwriting' if isfile(dst) else 'Creating',
dst))
render(src, dst, template)
Styles are omitted here to show the bare minimum template.
In the actual template, I use has_table
, has_form
, and has_toc
in the <style>
to keep the page size down a bit. I know, I could just use a linked stylesheet, but I prefer to instead keep my pages at only 1 request each. (Especially since my website is infrequently visited, so it won’t benefit much from caching.)
<!doctype html>
<html>
<!-- Auto-generated file, do not edit -->
<head>
<meta charset="utf-8">
<title>${title}</title>
<meta name="viewport" content="initial-scale=1">
</head>
<body>
${content}
</body>
</html>