#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os.path import shutil import email # using MIME format for src files import configparser from datetime import datetime from glob import glob import dateutil.parser import jinja2 import markdown from docutils.core import publish_parts OUTDIR = "public" SRCDIR = "src" TMPLDIR = "template" INLINECSS = "conf/templates/style.css" # XXX from Ikiwiki def ensure_dir(name): try: os.makedirs(name) except: pass TMPL_ENV = jinja2.Environment(loader=jinja2.FileSystemLoader(TMPLDIR)) EXTENSION_MAP = dict() def for_extension(ext): def decorate(func): EXTENSION_MAP[ext] = func return func return decorate @for_extension("css") @for_extension("png") @for_extension("jpg") @for_extension("webp") @for_extension("jpeg") @for_extension("webm") @for_extension("svg") @for_extension("pdf") @for_extension("js") @for_extension("html") @for_extension("py") @for_extension("ipynb") def hardlink_file(infile): assert (infile.startswith(SRCDIR)) outfile = os.path.join(OUTDIR,infile[len(SRCDIR)+1:]) os.makedirs(os.path.dirname(outfile), exist_ok=True) try: os.link(infile,outfile) except OSError: try: shutil.copy(infile,outfile) except shutil.SameFileError: # os.link above previously succeeded pass def load_infile(infile): with open(infile) as fh: raw = email.message_from_file(fh) assert(not raw.is_multipart()) vars = dict() for k,v in raw.items(): vars[k] = v vars['infile'] = infile vars = refurbish_vars(vars) contents = raw.get_payload() return contents, vars def outfile_with_ext(infile,new_ext): base = os.path.basename(infile) assert(infile.startswith(SRCDIR)) rel_dir = os.path.dirname(infile)[len(SRCDIR)+1:] root,ext = os.path.splitext(base) outfile = os.path.join(rel_dir, root) + new_ext dir = os.path.dirname(outfile) ensure_dir(dir) return outfile with open(INLINECSS) as fh: _INLINECSS_DATA = fh.read() def render(vars, outfile, tmpl_name="page.html"): vars["base_url"] = _CONFIG['base']['base_url'] vars["selfurl"] = os.path.join(vars["base_url"], outfile) vars["inlinecss"] = _INLINECSS_DATA vars["CONFIG"] = _CONFIG template = TMPL_ENV.get_template(tmpl_name) target = os.path.join(OUTDIR, outfile) with open(target, "w") as out: out.write(template.render(vars)) def cd_once(path): """Remove the first directory element of path""" i = path.find("/") if i > 0: return path[i+1:] return path assert cd_once("bla/foo/baz.txt") == "foo/baz.txt" def relative_prefix(infile): """Return relative path to root from infile""" assert (infile.startswith(SRCDIR+"/")) path = infile[len(SRCDIR)+1:] dir = os.path.dirname(path) depth = path.count("/") return "../" * depth def refurbish_vars(vars): infile = vars['infile'] vars["title"] = vars.get("title", "No Title?!") vars["author"] = vars.get("author", "unknown") vars["ROOT"] = relative_prefix(infile) if not "date" in vars: dt = datetime.fromtimestamp(os.path.getmtime(infile)) vars['date_parsed'] = dt vars["date"] = dt.strftime("%Y-%m-%d") else: vars['date_parsed'] = dateutil.parser.parse(vars['date']) return vars def needs_update(outfile, infile): """Returns if the outfile must be generated anew from the infile""" o = os.path.join(OUTDIR, outfile) try: mt_o = os.path.getmtime(o) except FileNotFoundError: return True mt_i = os.path.getmtime(infile) return mt_i > mt_o @for_extension("md") @for_extension("mdwn") @for_extension("markdown") def markdown_handler(infile): contents, vars = load_infile(infile) outfile = outfile_with_ext(infile,".html") if needs_update(outfile, infile): vars["contents"] = markdown.markdown(contents) render(vars, outfile) from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.directives import register_directive from docutils.writers.html4css1 import Writer, HTMLTranslator from docutils import nodes from docutils.nodes import TextElement from urllib.parse import quote class tweetable(TextElement): pass class Tweetable(BaseAdmonition): def run(self): self.assert_has_content() text = '\n'.join(self.content) admonition_node = nodes.tip(rawsource=text) self.state.nested_parse(self.content, self.content_offset, admonition_node) return [tweetable(text=text)] register_directive('tweetable', Tweetable) class exercise_elem(TextElement): pass class Exercise(BaseAdmonition): def run(self): self.assert_has_content() text = '\n'.join(self.content) admonition_node = exercise_elem(rawsource=text) self.state.nested_parse(self.content, self.content_offset, admonition_node) return [admonition_node] register_directive('exercise', Exercise) class MyHTMLTranslator(HTMLTranslator): def visit_tweetable(self, node): self.body.append(self.starttag(node, 'div', CLASS="tweetable")) text = quote(str(node[0])) via = "azwinkau" url = "https://twitter.com/intent/tweet?text=%s&url=%s;via=%s" % (text, _CONFIG['base']['base_url'], via) self.body.append(self.starttag(node, 'a', href=url)) self.body.append("Tweet This") self.body.append('') def depart_tweetable(self, node): self.body.append('') def visit_exercise_elem(self, node): self.body.append(self.starttag(node, 'div', CLASS="exercise")) self.body.append(self.starttag(node, 'span')) self.body.append("exercise") self.body.append('') def depart_exercise_elem(self, node): self.body.append('') def rst2html(txt): writer = Writer() writer.translator_class = MyHTMLTranslator p = publish_parts(txt, writer=writer, settings_overrides={'initial_header_level': 2}) return p['html_body'] @for_extension("htm") def htm_handler(infile): contents, vars = load_infile(infile) outfile = outfile_with_ext(infile,".html") if needs_update(outfile, infile): vars["contents"] = contents render(vars, outfile) @for_extension("rst") def rst_handler(infile): contents, vars = load_infile(infile) outfile = outfile_with_ext(infile,".html") if needs_update(outfile, infile): vars["contents"] = rst2html(contents) render(vars, outfile) @for_extension("collection") def collection_handler(infile): dirname = os.path.dirname(infile) # prefix for generator relpath = dirname[len(SRCDIR):] # prefix on website contents, vars = load_infile(infile) items = list() for filename in contents.split("\n"): f = os.path.join(dirname,filename) if not os.path.isfile(f): continue _, f_vars = load_infile(f) f_outfile = outfile_with_ext(f,".html") if relpath: f_outfile = f_outfile[len(relpath):] items.append(dict( title = f_vars['title'], link = f_outfile, date = f_vars['date'], date_parsed = f_vars['date_parsed'], tldr = f_vars.get('tldr', ""), image_src = f_vars.get('image_src', ""), )); items.sort(key = lambda x: x["date"], reverse=True) atom_outfile = outfile_with_ext(infile,".atom") vars["collection"] = items vars["atomlink"] = cd_once(atom_outfile) outfile = outfile_with_ext(infile,".html") render(vars, outfile, tmpl_name="collection.html") render(vars, atom_outfile, tmpl_name="collection.atom") def all_infiles(): for root, dirnames, filenames in os.walk(SRCDIR): for filename in filenames: yield os.path.join(root, filename) _CONFIG = configparser.ConfigParser() if __name__ == "__main__": ensure_dir(OUTDIR) _CONFIG.read("config.ini") for infile in all_infiles(): if not os.path.isfile(infile): continue root,ext = os.path.splitext(os.path.basename(infile)) if not ext[1:] in EXTENSION_MAP.keys(): print("unknown extension:", infile) continue EXTENSION_MAP[ext[1:]](infile)