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