__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 83def __str__(self): 84return self.content 85 86def __getitem__(self, item): 87return self.front_matter[item] 88 89 90class Index: 91def __init__(self, directory, recursive=False, url_transform=lambda x: x): 92self.directory = directory 93# Temporarily move to the specified directory in order to read the files. 94with in_directory(directory): 95if recursive: 96self.file_names = [os.path.join(dir_path, f) for dir_path, dir_name, filenames in os.walk(".") for f in filenames] 97else: 98self.file_names = [i for i in os.listdir() if os.path.isfile(i)] 99self.documents = [Document(i, url_transform) for i in self.file_names] 100self.__current_index = 0 101 102def __iter__(self): 103self.__current_index = 0 104return self 105 106def __next__(self): 107if self.__current_index >= len(self.documents): 108raise StopIteration 109else: 110self.__current_index += 1 111return self.documents[self.__current_index - 1] 112 113def __repr__(self): 114return f"Index({self.directory}): {self.documents}" 115 116def __len__(self): 117return len(self.documents) 118 119 120class Site: 121def __init__(self, build_dir, template_dir="templates"): 122self.build_dir = build_dir 123self.template_engine = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) 124self.pages = {} 125self.context = {} 126 127def add_page(self, location, page): 128if location.endswith("/"): 129location += "index.html" 130location = location.lstrip("/") # interpret it as site root, not OS root 131self.pages[location] = page 132 133def add_from_index(self, index, location, template, static=False, **kwargs): 134location = location.lstrip("/") # interpret it as site root, not OS root 135kwargs = {**self.context, **kwargs} 136if static: 137for document in index: 138self.pages[os.path.join(location, document.file_name)] = Static(self, document) 139else: 140for document in index: 141self.pages[os.path.join(location, document.file_name)] = Page(self, template, document, **kwargs) 142 143def filter(self, name): 144def decorator(func): 145self.template_engine.filters[name] = func 146return func 147 148return decorator 149 150def build(self): 151# Clear the build directory if it exists. 152if os.path.isdir(self.build_dir): 153delete_directory_contents(self.build_dir) 154for location, page in self.pages.items(): 155# Create the required directories. 156os.makedirs(os.path.join(self.build_dir, os.path.dirname(location)), exist_ok=True) 157if isinstance(page, str): 158with open(os.path.join(self.build_dir, location), "w") as f: 159f.write(page) 160elif isinstance(page, bytes): 161with open(os.path.join(self.build_dir, location), "wb") as f: 162f.write(page) 163else: 164raise ValueError(f"{type(page)} cannot be used as a document") 165 166 167class Page(str): 168def __new__(cls, site, template, document=None, **kwargs): 169kwargs = {**site.context, **kwargs} 170return site.template_engine.get_template(template).render(document=document, **kwargs) 171 172 173class Static(bytes): 174def __new__(cls, site, document): 175return document.content 176