From a8c4d6ef483d34e3a39cd67c0e7df3e3ca6b3714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 27 Jun 2022 20:39:21 +0200 Subject: [PATCH] Add base Markdown blog structure. --- scripts/blog.py | 157 +++++++++++++++++++++++++++++++++++++++ scripts/requirements.txt | 2 + static/article.tmpl.html | 15 ++++ static/blog.css | 6 ++ 4 files changed, 180 insertions(+) create mode 100644 scripts/blog.py create mode 100644 scripts/requirements.txt create mode 100644 static/article.tmpl.html create mode 100644 static/blog.css diff --git a/scripts/blog.py b/scripts/blog.py new file mode 100644 index 0000000..2e1510c --- /dev/null +++ b/scripts/blog.py @@ -0,0 +1,157 @@ +#!/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]) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..9878f29 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +Markdown +Jinja2 diff --git a/static/article.tmpl.html b/static/article.tmpl.html new file mode 100644 index 0000000..d535fbf --- /dev/null +++ b/static/article.tmpl.html @@ -0,0 +1,15 @@ + + + + + Código para llevar + + + + + +
+ {{ content | safe }} +
+ + diff --git a/static/blog.css b/static/blog.css new file mode 100644 index 0000000..eb9a2ef --- /dev/null +++ b/static/blog.css @@ -0,0 +1,6 @@ +body { + margin: 0 auto; + width: fit-content; + max-width: 100ex; + padding: 0 1ex; +}