__init__.py
Python script, ASCII text executable
1#!/usr/bin/env python3 2 3import os 4import jinja2 5from ruamel.yaml import YAML 6import shutil 7import contextlib 8 9 10@contextlib.contextmanager 11def in_directory(directory): 12cwd = os.getcwd() 13os.chdir(directory) 14try: 15yield 16finally: 17os.chdir(cwd) 18 19 20def delete_directory_contents(directory): 21for root, dirs, files in os.walk(directory): 22for file in files: 23os.remove(os.path.join(root, file)) 24for dir in dirs: 25shutil.rmtree(os.path.join(root, dir)) 26 27 28class Document: 29def __init__(self, file_name): 30self.file_name = file_name 31self.encoding = "utf-8" 32# If the file is text, read it. 33self.front_matter = YAML() 34self.content = "" 35try: 36with open(file_name, "r", encoding=self.encoding) as f: 37print("!" + f"Loading document {file_name}".center(80, "-") + "!") 38 39# Parse front matter if available. 40front_matter = "" 41initial_line = f.readline() 42if initial_line == "---\n": 43print("!" + "Front matter found".center(80, "-") + "!") 44line = "" 45while line != "---\n": 46line = f.readline() 47if line != "---\n": 48front_matter += line 49print("!" + "Front matter loaded".center(80, "-") + "!") 50print(front_matter) 51 52if front_matter: 53self.front_matter = self.front_matter.load(front_matter) 54else: # put it back 55self.content = initial_line 56 57print("!" + "Reading content".center(80, "-") + "!") 58 59self.content += f.read() 60 61print("!" + "Content loaded".center(80, "-") + "!") 62print(self.content[:256] + "..." if len(self.content) > 256 else self.content) 63except UnicodeDecodeError: 64print("!" + "Text decoding failed, assuming binary.".center(80, "-") + "!") 65self.encoding = None 66with open(file_name, "rb") as f: 67self.content = f.read() 68print("!" + "Binary content loaded".center(80, "-") + "!") 69 70 71class Index: 72def __init__(self, directory, recursive=False, url_transform=lambda x: x): 73self.directory = directory 74# Temporarily move to the specified directory in order to read the files. 75with in_directory(directory): 76if recursive: 77self.file_names = [os.path.join(dir_path, f) for dir_path, dir_name, filenames in os.walk(".") for f in filenames] 78else: 79self.file_names = [i for i in os.listdir() if os.path.isfile(i)] 80self.documents = [Document(url_transform(i)) for i in self.file_names] 81self.__current_index = 0 82 83def __iter__(self): 84return self 85 86def __next__(self): 87if self.__current_index >= len(self.documents): 88raise StopIteration 89else: 90self.__current_index += 1 91return self.documents[self.__current_index - 1] 92 93 94class Site: 95def __init__(self, build_dir, template_dir="templates"): 96self.build_dir = build_dir 97self.template_engine = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) 98self.pages = {} 99self.context = {} 100 101def add_page(self, location, page): 102if location.endswith("/"): 103location += "index.html" 104location = location.lstrip("/") # interpret it as site root, not OS root 105self.pages[location] = page 106 107def add_from_index(self, index, location, template, static=False, **kwargs): 108location = location.lstrip("/") # interpret it as site root, not OS root 109kwargs = {**self.context, **kwargs} 110if static: 111for document in index: 112self.pages[os.path.join(location, document.file_name)] = Static(self, document) 113else: 114for document in index: 115self.pages[os.path.join(location, document.file_name)] = Page(self, template, document, **kwargs) 116 117def filter(self, name): 118def decorator(func): 119self.template_engine.filters[name] = func 120return func 121 122return decorator 123 124def build(self): 125# Clear the build directory if it exists. 126if os.path.isdir(self.build_dir): 127delete_directory_contents(self.build_dir) 128for location, page in self.pages.items(): 129# Create the required directories. 130os.makedirs(os.path.join(self.build_dir, os.path.dirname(location)), exist_ok=True) 131if isinstance(page, str): 132with open(os.path.join(self.build_dir, location), "w") as f: 133f.write(page) 134elif isinstance(page, bytes): 135with open(os.path.join(self.build_dir, location), "wb") as f: 136f.write(page) 137else: 138raise ValueError(f"{type(page)} cannot be used as a document") 139 140 141class Page(str): 142def __new__(cls, site, template, document=None, **kwargs): 143kwargs = {**site.context, **kwargs} 144return site.template_engine.get_template(template).render(document=document, **kwargs) 145 146 147class Static(bytes): 148def __new__(cls, site, document): 149return document.content 150