#!/usr/bin/env python3 MARKDOWN_EXTENSION = '.md' EXTENSIONS = [ MARKDOWN_EXTENSION, ] MARKDOWN_EXTRA_FEATURES = [ # See more in: https://python-markdown.github.io/extensions/ 'markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.extra', ] import json import logging import sys import os import datetime import jinja2 import shutil import yaml import markdown import re from unidecode import unidecode NIKOLA_DATE_RE = re.compile(r'^([0-2]\d|30|31)\.(0\d|1[012])\.(\d{4}), (\d{1,2}):(\d{2})$') COMPLETE_DATE_RE = re.compile(r'^(\d{4})-(0\d|1[012])-([0-2]\d|30|31) ' + r'(\d{2}):(\d{2})(:\d{2})( .+)?$') SLUG_HYPHENATE_RE = re.compile(r'[\s\-]+') SLUG_REMOVE_RE = re.compile(r'[^\s\-a-zA-Z0-9]*') ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_PATH = os.path.join(ROOT_DIR, 'static') ARTICLE_TEMPLATE_NAME = 'article.tmpl.html' STATIC_RESOURCES = ( ('style.css', 'css/style.css'), ) JINJA_ENV = jinja2.Environment( loader=jinja2.FileSystemLoader(STATIC_PATH), autoescape=jinja2.select_autoescape() ) ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME) def parse_nikola_date(match): return datetime.datetime(year=int(match.group(3)), month=int(match.group(2)), day=int(match.group(1)), hour=int(match.group(4)), minute=int(match.group(5))) def parse_complete_date(match): return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z') def slugify(title): """ Made for compatibility with Nikola's slugify within CodigoParaLlevar blog. """ slug = unidecode(title).lower() slug = SLUG_REMOVE_RE.sub('', slug) slug = SLUG_HYPHENATE_RE.sub('-', slug) return slug.strip() def read_markdown(path): with open(path, 'rt') as f: data = f.read() if data.startswith('---'): start = data.index('\n') if '---\n' not in data[start:]: raise Exception('Front matter not finished on: {}'.format(path)) front_matter_str, content = data[start:].split('---\n', 1) front_matter = yaml.load(front_matter_str, Loader=yaml.SafeLoader) else: raise Exception('Front matter is needed for proper rendering. Not found on: {}'.format( path )) doc = markdown.markdown(content, extensions=MARKDOWN_EXTRA_FEATURES) return doc, front_matter def get_out_path(front_matter): if 'date' in front_matter: if m := NIKOLA_DATE_RE.match(front_matter['date']): front_matter['date'] = parse_nikola_date(m) elif m := COMPLETE_DATE_RE.match(front_matter['date']): front_matter['date'] = parse_complete_date(m) else: raise NotImplementedError('Unknown date format: {}'.format( front_matter['date'])) else: raise Exception('No date found on: {}'.format( path )) if 'slug' not in front_matter: if 'title' not in front_matter: raise Exception('No title found on: {}'.format( path )) front_matter['slug'] = slugify(front_matter['title']) out_path = os.path.join(str(front_matter['date'].year), front_matter['slug']) return out_path def load_all(top_dir_relative): top = os.path.abspath(top_dir_relative) docs = [] for root, dirs, files in os.walk(top): for name in files: if all([not name.endswith(ext) for ext in EXTENSIONS]): # The logic is negative... but it works continue if name.endswith(MARKDOWN_EXTENSION): path = os.path.join(root, name) doc, front_matter = read_markdown(path) out_path = get_out_path(front_matter) docs.append((doc, front_matter, out_path)) else: raise NotImplementedError('Unknown filetype: {}'.format(name)) return docs def render_article(doc, f): result = ARTICLE_TEMPLATE.render(content=doc) f.write(result) def main(source_top, dest_top): docs = load_all(source_top) for (doc, front_matter, out_path) in docs: doc_full_path = os.path.join(dest_top, out_path) os.makedirs(os.path.dirname(doc_full_path), exist_ok=True) print("==", doc_full_path) with open(doc_full_path + '.html', 'wt') as f: render_article(doc, f) for src, dest in STATIC_RESOURCES: target_dest = os.path.join(dest_top, dest) os.makedirs(os.path.dirname(target_dest), exist_ok=True) shutil.copy(os.path.join(STATIC_PATH, src), target_dest) if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0])) exit(0) logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") main(sys.argv[1], sys.argv[2])