~eightytwo/idspispopd

ff022547ed3daf91b4f41e7eb378c2dae2732e9f — eightytwo 4 years ago 3fe8a5a
Add the site builder code and requirements
2 files changed, 213 insertions(+), 0 deletions(-)

A build.py
A requirements.txt
A build.py => build.py +207 -0
@@ 0,0 1,207 @@
import os
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime
from os.path import isfile
from os.path import join
from pathlib import Path
from shutil import copytree
from typing import Dict
from typing import List
from typing import Tuple

import markdown
from markdown.extensions.toc import TocExtension
from markdown.extensions.codehilite import CodeHiliteExtension
from jinja2 import Environment, FileSystemLoader
from slugify import slugify


@dataclass(frozen=True)
class Page:
    category: str
    template: str
    listing: bool = False
    create_detail_pages: bool = False
    detail_page_template: str = None
    source_dir: str = None


BUILD_PATH = 'build'
STATIC_ASSETS_PATH = 'static'
TEMPLATES_PATH = 'templates'

PAGES = [
    Page(category='404', template='404'),
    Page(category='about', template='about'),
    Page(category='blog',
         template='blog',
         listing=True,
         create_detail_pages=True,
         detail_page_template='post',
         source_dir='content/blog'),
    Page(category='projects',
         template='projects',
         listing=True,
         source_dir='content/projects'),
]


def write_file(name: str, content: str):
    """Write an HTML file to disk.

    :param name: The name of the file, excluding extension.
    :param content: The content of the file.
    """
    filepath = Path(f'{BUILD_PATH}/{name}.html')
    with open(filepath, "w+", encoding="utf-8", errors="xmlcharrefreplace") as f:
        f.write(content)


def parse_markdown(filepath: str) -> Dict:
    """Parse a Markdown file.

    :param filepath: The path and filename of the Markdown file to be parsed.
    :return: A dictionary containing the metadata and content of the Markdown file.
    """
    data = Path(filepath).read_text(encoding='utf-8')
    md = markdown.Markdown(
        extensions=[
            'meta',
            TocExtension(title='Contents', permalink=True),
            CodeHiliteExtension(guess_lang=False),
        ],
        output_format='html5'
    )

    return {
        'html': md.convert(data),
        'metadata': md.Meta,
    }


def get_page_vars(filepath: str) -> Dict:
    """Extract variables from a Markdown file, such as the metadata fields and the
    content of the file.

    :param filepath: The path and filename of the Markdown file to be processed.
    :return: A dictionary containing the page variables.
    """
    data = parse_markdown(filepath)
    template_vars = {'content': data['html']}

    for field, value in data['metadata'].items():
        if field == 'date_published':
            template_vars[field] = datetime.strptime(value[0], '%d %b %Y')
        else:
            template_vars[field] = value[0] if len(value) == 1 else value

    return template_vars


def build_tag_page(page_category: str, tag: str, pages: List[Dict]):
    """Create an HTML page that lists items with a given tag.

    :param page_category: The type of page, such as 'blog' or 'project'.
    :param tag: The tag name.
    :param pages: A list of pages that have the given tag. Each page is a
     dictionary of page variables, extracted from the page's corresponding
     Markdown file.
    """
    # Items on a page, such as blog posts or projects, are listed from most to
    # least recent.
    items = sorted(pages, key=lambda x: x['date_published'], reverse=True)

    template = env.get_template(f'{page_category}.html')
    write_file(
        f'{page_category}/tag/{tag}',
        template.render(category=page_category, tag=tag, items=items)
    )


def build_detail_pages(parent_page, detail_pages: List[Dict]):
    for detail_page in detail_pages:
        template = env.get_template(f'{parent_page.detail_page_template}.html')
        write_file(
            f"{parent_page.category}/{detail_page['slug']}",
            template.render(category=parent_page.category, page=detail_page)
        )


def build_list_page(page: Page) -> Tuple[List[Dict], Dict]:
    """Create an HTML page that lists items, such as blog posts or projects.

    :param page: The page to be created.
    :return:
    """
    items = []
    tags = defaultdict(list)

    for f in os.listdir(page.source_dir):
        filepath = join(page.source_dir, f)

        if not isfile(filepath):
            continue

        page_vars = get_page_vars(filepath)

        if 'title' in page_vars:
            page_vars['slug'] = slugify(page_vars['title'])

        items.append(page_vars)

        if 'tags' in page_vars:
            for tag in page_vars['tags']:
                tags[tag].append(page_vars)

    # Items on a page, such as blog posts or projects, are listed from most to
    # least recent.
    items = sorted(items, key=lambda x: x['date_published'], reverse=True)

    template = env.get_template(f'{page.template}.html')
    write_file(page.category, template.render(category=page.category, items=items))

    return items, tags


def build_simple_page(page: Page):
    """Create an HTML page from a template.

    :param page_name: The name of the page to build, excluding the extension.
    """
    template = env.get_template(f'{page.template}.html')
    write_file(page.category, template.render(category=page.category))


if __name__ == "__main__":
    env = Environment(
        loader=FileSystemLoader(searchpath=TEMPLATES_PATH),
        trim_blocks=True,
        lstrip_blocks=True
    )

    # Ensure some build directories exist
    Path(f'{BUILD_PATH}/blog/tag/').mkdir(parents=True, exist_ok=True)

    # A place to keep track of all tags
    all_tags = defaultdict(list)

    # Build each top level page
    for page in PAGES:
        if page.listing:
            items, tags = build_list_page(page)

            if page.create_detail_pages:
                build_detail_pages(page, items)

            for tag, page_vars in tags.items():
                all_tags[(page.category, tag)].extend(page_vars)
        else:
            build_simple_page(page)

    # Build the tag pages
    for (page_category, tag), page_vars in all_tags.items():
        build_tag_page(page_category, tag, page_vars)

    # Copy the static files to the build directory
    copytree(STATIC_ASSETS_PATH, BUILD_PATH, dirs_exist_ok=True)

A requirements.txt => requirements.txt +6 -0
@@ 0,0 1,6 @@
Jinja2==2.11.2
Markdown==3.2.2
MarkupSafe==1.1.1
Pygments==2.6.1
python-slugify==4.0.0
text-unidecode==1.3