By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 __init__.py

View raw Download
text/x-script.python • 10.12 kiB
Python script, ASCII text executable
        
            
1
#!/usr/bin/env python3
2
3
import os
4
import re
5
import shutil
6
import contextlib
7
import typing
8
import jinja2
9
import colorama
10
from datetime import datetime
11
from ruamel.yaml import YAML
12
from ampoule_ssg._utils import *
13
14
15
colorama.init()
16
17
18
class Document:
19
"""A type representing a document, which can be text or binary."""
20
def __init__(self, file_name: typing.Union[str, bytes, os.PathLike], url_transform: typing.Callable = lambda x: x, front_matter_enabled: bool = True):
21
"""Create a new document object.
22
23
:param file_name: The name of the file to read.
24
:param url_transform: Function to change the file name to a different form.
25
"""
26
self.file_name = file_name
27
self.encoding = "utf-8"
28
# If the file is text, read it.
29
self.front_matter = YAML()
30
self.front_matter.Constructor.add_constructor("tag:yaml.org,2002:timestamp", _no_date_constructor)
31
self.content = ""
32
self.date = datetime.fromtimestamp(os.path.getmtime(file_name))
33
try:
34
with open(file_name, "r", encoding=self.encoding) as f:
35
print(colorama.Style.RESET_ALL, colorama.Style.BRIGHT, colorama.Fore.LIGHTWHITE_EX, f"Loading document {file_name}".ljust(shutil.get_terminal_size().columns), sep="")
36
37
# Parse front matter if available.
38
front_matter = ""
39
if front_matter_enabled:
40
initial_line = f.readline()
41
if initial_line == "---\n":
42
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Front matter found", sep="")
43
line = ""
44
while line != "---\n":
45
line = f.readline()
46
if line != "---\n":
47
front_matter += line
48
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Front matter loaded", sep="")
49
50
if front_matter and front_matter_enabled:
51
self.front_matter = self.front_matter.load(front_matter)
52
53
print(self.front_matter, type(self.front_matter))
54
55
if "DATE" in self.front_matter:
56
self.date = parse_date_string(self.front_matter["DATE"])
57
elif front_matter_enabled: # put it back
58
self.content = initial_line
59
60
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Reading content", sep="")
61
62
self.content += f.read()
63
64
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Content loaded", sep="")
65
print(colorama.Style.RESET_ALL, colorama.Style.DIM, self.content[:128] + "..." if len(self.content) > 128 else self.content)
66
except UnicodeDecodeError:
67
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Text decoding failed, assuming binary", sep="")
68
self.encoding = None
69
with open(file_name, "rb") as f:
70
self.content = f.read()
71
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Binary content loaded", sep="")
72
73
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, colorama.Style.DIM, f"Transforming URL {self.file_name} ->", end=" ", sep="")
74
self.file_name = url_transform(self.file_name)
75
print(colorama.Style.RESET_ALL, colorama.Style.BRIGHT, colorama.Fore.LIGHTYELLOW_EX, self.file_name)
76
77
print(colorama.Style.RESET_ALL, end="")
78
79
def __repr__(self):
80
return f"Document({self.file_name})"
81
82
def __str__(self):
83
return self.content
84
85
def __getitem__(self, item: str):
86
"""Get an item from the front matter of the document"""
87
return self.front_matter[item]
88
89
def __setitem__(self, item: str, value: typing.Any):
90
"""Set an item in the front matter of the document"""
91
self.front_matter[item] = value
92
93
def __delitem__(self, item: str):
94
"""Delete an item from the front matter of the document"""
95
del self.front_matter[item]
96
97
def __contains__(self, item: str):
98
"""Check if an item is in the front matter of the document"""
99
return item in self.front_matter
100
101
102
class Index:
103
"""A type representing an index of documents."""
104
def __init__(self, directory: typing.Union[str, bytes, os.PathLike], recursive: bool = False,
105
url_transform: typing.Callable = lambda x: x, sort_by: typing.Callable = lambda x: x.file_name, reverse: bool = False,
106
exclude: typing.Union[str, None] = None, static: bool = False):
107
"""Create a new index object.
108
109
:param directory: The directory to read the files from.
110
:param recursive: Whether to read files from subdirectories.
111
:param url_transform: Function to change the file name to a different form.
112
:param sort_by: Function returning a key to sort the documents by.
113
:param exclude: Regular expression to exclude files from the index.
114
"""
115
self.directory = directory
116
self.static = static
117
# Temporarily move to the specified directory in order to read the files.
118
if exclude:
119
regex = re.compile(exclude)
120
else:
121
regex = re.compile("(?!)")
122
with _in_directory(directory):
123
if recursive:
124
self.file_names = [os.path.join(dir_path, f) for dir_path, dir_name, filenames in os.walk(".") for f in filenames if not regex.search(f)]
125
else:
126
self.file_names = [i for i in os.listdir() if os.path.isfile(i) and not regex.search(i)]
127
128
self.documents = sorted([Document(i, url_transform, front_matter_enabled=not static) for i in self.file_names], key=sort_by, reverse=reverse)
129
self.__current_index = 0
130
131
def __iter__(self):
132
self.__current_index = 0
133
return self
134
135
def __next__(self):
136
if self.__current_index >= len(self.documents):
137
raise StopIteration
138
else:
139
self.__current_index += 1
140
return self.documents[self.__current_index - 1]
141
142
def __repr__(self):
143
return f"Index({self.directory}): {self.documents}"
144
145
def __len__(self):
146
return len(self.documents)
147
148
149
class Site:
150
"""A type representing a website."""
151
def __init__(self, build_dir: typing.Union[str, bytes, os.PathLike], template_dir: typing.Union[str, bytes, os.PathLike] = "templates"):
152
"""Create a new site object.
153
154
:param build_dir: The directory to build the site in.
155
:param template_dir: The directory to read the templates from.
156
"""
157
self.build_dir: str = build_dir
158
self.template_engine: jinja2.Environment = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))
159
self.pages: dict[str, typing.Union[Static, Page]] = {}
160
self.context: dict[str, typing.Any] = {}
161
162
def add_page(self, location: typing.Union[str, bytes, os.PathLike], page: typing.Union["Static", "Page"]):
163
"""Add a page to the site.
164
165
:param location: The location the page should be saved to.
166
:param page: The page object itself.
167
"""
168
if location.endswith("/"):
169
location += "index.html"
170
location = location.lstrip("/") # interpret it as site root, not OS root
171
self.pages[location] = page
172
173
def add_from_index(self, index: Index, location: typing.Union[str, bytes, os.PathLike], template: typing.Union[str, None] = None, **kwargs):
174
"""Add pages to the site from an index.
175
176
:param index: The index to read the documents from.
177
:param location: The location to save the pages to.
178
:param template: The template to use for the pages.
179
:param static: Whether to treat them as static files.
180
:param kwargs: Additional keyword arguments to pass to the template when rendering.
181
"""
182
location = location.lstrip("/") # interpret it as site root, not OS root
183
kwargs = {**self.context, **kwargs}
184
if index.static:
185
for document in index:
186
self.pages[os.path.join(location, document.file_name)] = Static(self, document)
187
else:
188
for document in index:
189
self.pages[os.path.join(location, document.file_name)] = Page(self, template, document, **kwargs)
190
191
def filter(self, name: str):
192
"""Decorator to add a filter to the template engine.
193
194
:param name: The name the filter will be used with in Jinja2.
195
"""
196
def decorator(func):
197
self.template_engine.filters[name] = func
198
return func
199
200
return decorator
201
202
def test(self, name: str):
203
"""Decorator to add a test to the template engine.
204
205
:param name: The name the test will be used with in Jinja2.
206
"""
207
def decorator(func):
208
self.template_engine.tests[name] = func
209
return func
210
211
return decorator
212
213
def build(self, dont_delete: typing.Optional[list[str]] = None):
214
"""Build the site in its directory."""
215
# Clear the build directory if it exists.
216
if os.path.isdir(self.build_dir):
217
_delete_directory_contents(self.build_dir, dont_delete=dont_delete)
218
for location, page in self.pages.items():
219
# Create the required directories.
220
os.makedirs(os.path.join(self.build_dir, os.path.dirname(location)), exist_ok=True)
221
if isinstance(page, str):
222
with open(os.path.join(self.build_dir, location), "w") as f:
223
f.write(page)
224
elif isinstance(page, bytes):
225
with open(os.path.join(self.build_dir, location), "wb") as f:
226
f.write(page)
227
else:
228
raise ValueError(f"{type(page)} cannot be used as a document")
229
230
231
class Page(str):
232
"""A type representing a page, which is a rendered template."""
233
def __new__(cls, site: Site, template: str, document: Document = None, **kwargs):
234
kwargs = {**site.context, **kwargs}
235
return site.template_engine.get_template(template).render(document=document, **kwargs)
236
237
238
class Static(bytes):
239
"""A type representing a static file, which is binary content that is not templated."""
240
def __new__(cls, site: Site, document: Document):
241
return document.content
242