@@ 1,9 1,6 @@
-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
@@ 15,6 12,7 @@ import markdown
from jinja2 import Environment
from jinja2 import FileSystemLoader
from markdown.extensions.codehilite import CodeHiliteExtension
+from markdown.extensions.footnotes import FootnoteExtension
from markdown.extensions.toc import TocExtension
from slugify import slugify
@@ 26,7 24,7 @@ class Page:
listing: bool = False
create_detail_pages: bool = False
detail_page_template: Optional[str] = None
- source_dir: Optional[str] = None
+ source_dir: Optional[Path] = None
BUILD_PATH = 'build'
@@ 42,40 40,41 @@ PAGES = [
listing=True,
create_detail_pages=True,
detail_page_template='post',
- source_dir='content/blog',
+ source_dir=Path('content/blog'),
),
Page(
category='projects',
template='projects',
listing=True,
- source_dir='content/projects',
+ source_dir=Path('content/projects'),
),
]
-def write_file(name: str, content: str):
+def write_file(name: Path, content: str, suffix: str = ".html"):
"""Write an HTML file to disk.
- :param name: The name of the file, excluding extension.
+ :param name: The path and name of the file, excluding extension.
:param content: The content of the file.
+ :param suffix: The extension of the file.
"""
- filepath = Path(f'{BUILD_PATH}/{name}.html')
- with open(filepath, "w+", encoding="utf-8", errors="xmlcharrefreplace") as f:
- f.write(content)
+ file = BUILD_PATH / name.with_suffix(suffix)
+ file.write_text(content, encoding="utf-8", errors="xmlcharrefreplace")
-def parse_markdown(filepath: str) -> Dict:
+def parse_markdown(filepath: Path) -> 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')
+ data = filepath.read_text(encoding='utf-8')
md = markdown.Markdown(
extensions=[
'meta',
TocExtension(title='Contents', permalink=True, toc_depth=2),
CodeHiliteExtension(guess_lang=False),
+ FootnoteExtension(),
],
output_format='html5',
)
@@ 86,7 85,7 @@ def parse_markdown(filepath: str) -> Dict:
}
-def get_page_vars(filepath: str) -> Dict:
+def get_page_vars(filepath: Path) -> Dict:
"""Extract variables from a Markdown file, such as the metadata fields and the
content of the file.
@@ 97,7 96,7 @@ def get_page_vars(filepath: str) -> Dict:
template_vars = {'content': data['html']}
for field, value in data['metadata'].items():
- if field == 'date_published':
+ if field.startswith('date_'):
template_vars[field] = datetime.strptime(value[0], '%d %b %Y')
else:
template_vars[field] = value[0] if len(value) == 1 else value
@@ 120,53 119,72 @@ def build_tag_page(page_category: str, tag: str, pages: List[Dict]):
template = env.get_template(f'{page_category}.html')
write_file(
- f'{page_category}/tag/{tag}',
+ Path(page_category, "tag", tag),
template.render(category=page_category, tag=tag, items=items),
)
def build_detail_pages(parent_page, detail_pages: List[Dict]):
+ """Create an HTML page for each item that appears in a parent list page.
+
+ :param parent_page: The page that displays a list of items.
+ :param detail_pages: The data for the list items that will have pages
+ created for them.
+ """
for detail_page in detail_pages:
+ output_file = Path(parent_page.category, detail_page['slug'])
template = env.get_template(f'{parent_page.detail_page_template}.html')
write_file(
- f"{parent_page.category}/{detail_page['slug']}",
+ output_file,
template.render(category=parent_page.category, page=detail_page),
)
+ # Check if this detail page (e.g. a blog post page or project page) has any
+ # assets, such as images or videos. If so, copy these assets to the build
+ # directory.
+ assets_dir = parent_page.source_dir / detail_page['filename'].stem
+ if assets_dir.is_dir():
+ copytree(assets_dir, Path(BUILD_PATH) / output_file, dirs_exist_ok=True)
+
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 that appear on the list page are gathered into a list so individual
+ pages can be created. While processing these items, a map of tags is also
+ built.
+
+ :param page: The list page to be created.
+ :return: The items that appear in the list along with a map of tags.
"""
items = []
tags: Dict[str, List] = defaultdict(list)
- source_dir = str(page.source_dir)
- for f in os.listdir(source_dir):
- filepath = join(source_dir, f)
+ if page.source_dir:
+ for file in page.source_dir.iterdir():
+ if not file.is_file():
+ continue
- if not isfile(filepath):
- continue
+ page_vars = get_page_vars(file)
+ page_vars['filename'] = file
- page_vars = get_page_vars(filepath)
+ if 'title' in page_vars:
+ page_vars['slug'] = slugify(page_vars['title'])
- if 'title' in page_vars:
- page_vars['slug'] = slugify(page_vars['title'])
+ items.append(page_vars)
- items.append(page_vars)
-
- if 'tags' in page_vars:
- for tag in page_vars['tags']:
- tags[tag].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))
+ write_file(
+ Path(page.category), template.render(category=page.category, items=items)
+ )
return items, tags
@@ 174,20 192,23 @@ def build_list_page(page: Page) -> Tuple[List[Dict], Dict]:
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.
+ :param page: The page to build.
"""
template = env.get_template(f'{page.template}.html')
- write_file(page.category, template.render(category=page.category))
+ write_file(Path(page.category), template.render(category=page.category))
if __name__ == "__main__":
env = Environment(
- autoescape=True,
loader=FileSystemLoader(searchpath=TEMPLATES_PATH),
+ autoescape=True,
trim_blocks=True,
lstrip_blocks=True,
)
+ # Copy the static files to the build directory
+ copytree(STATIC_ASSETS_PATH, BUILD_PATH, dirs_exist_ok=True)
+
# Ensure some build directories exist
Path(f'{BUILD_PATH}/blog/tag/').mkdir(parents=True, exist_ok=True)
@@ 210,6 231,3 @@ if __name__ == "__main__":
# 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)