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 • 9.07 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
from datetime import datetime
8
import jinja2
9
from ruamel.yaml import YAML
10
import colorama
11
12
13
colorama.init()
14
15
16
# Disable YAML date constructor
17
def no_date_constructor(loader, node):
18
value = loader.construct_scalar(node)
19
return value
20
21
22
@contextlib.contextmanager
23
def in_directory(directory):
24
cwd = os.getcwd()
25
os.chdir(directory)
26
try:
27
yield
28
finally:
29
os.chdir(cwd)
30
31
32
def delete_directory_contents(directory):
33
for root, dirs, files in os.walk(directory):
34
for file in files:
35
os.remove(os.path.join(root, file))
36
for dir in dirs:
37
shutil.rmtree(os.path.join(root, dir))
38
39
40
def parse_date_string(date_string):
41
def split_date_and_time(date_string):
42
if ":" not in date_string:
43
return date_string, "00:00:00"
44
45
elements = date_string.partition(":")
46
partition_character = " "
47
if " " not in date_string:
48
partition_character = "-"
49
if "-" not in date_string:
50
partition_character = "T"
51
52
date = elements[0].rpartition(partition_character)[0].strip()
53
time = elements[0].rpartition(partition_character)[2].strip() + elements[1] + elements[2].strip()
54
time = time.removeprefix("T").removesuffix("Z")
55
56
return date, time
57
58
time_formats = [
59
# 24-hour ISO
60
"%H:%M:%S",
61
"%H:%M",
62
"%H",
63
# Single digit hour
64
"-%H:%M:%S",
65
"-%H:%M",
66
"-%H",
67
# 12-hour (AM/PM)
68
"%I:%M:%S %p",
69
"%I:%M %p",
70
"%I %p",
71
# Single digit 12-hour
72
"-%I:%M:%S %p",
73
"-%I:%M %p",
74
"-%I %p",
75
]
76
77
date_formats = [
78
# ISO formats
79
"%Y-%m-%d",
80
"%y-%m-%d",
81
# European formats
82
"%d.%m.%Y",
83
"%d.%m.%y",
84
# American formats
85
"%m/%d/%Y",
86
"%m/%d/%y",
87
# Text-based European formats
88
"%d %B %Y",
89
"%d %b %Y",
90
"%d %B, %Y",
91
"%d %b, %Y",
92
# Text-based American formats
93
"%B %d %Y",
94
"%b %d %Y",
95
"%B %d, %Y",
96
"%b %d, %Y",
97
# ISO weekly calendar
98
"%G-W%V-%u",
99
]
100
101
date, time = split_date_and_time(date_string)
102
103
time_object = datetime.min.time()
104
date_object = datetime.min.date()
105
106
for time_format in time_formats:
107
try:
108
time_object = datetime.strptime(time, time_format)
109
except ValueError:
110
pass
111
for date_format in date_formats:
112
try:
113
date_object = datetime.strptime(date, date_format)
114
except ValueError:
115
pass
116
117
return datetime.combine(date_object, time_object.time())
118
119
120
class Document:
121
def __init__(self, file_name, url_transform=lambda x: x):
122
self.file_name = file_name
123
self.encoding = "utf-8"
124
# If the file is text, read it.
125
self.front_matter = YAML()
126
self.front_matter.Constructor.add_constructor("tag:yaml.org,2002:timestamp", no_date_constructor)
127
self.content = ""
128
self.date = datetime.fromtimestamp(os.path.getmtime(file_name))
129
try:
130
with open(file_name, "r", encoding=self.encoding) as f:
131
print(colorama.Style.RESET_ALL, colorama.Style.BRIGHT, colorama.Fore.LIGHTWHITE_EX, f"Loading document {file_name}".ljust(shutil.get_terminal_size().columns), sep="")
132
133
# Parse front matter if available.
134
front_matter = ""
135
initial_line = f.readline()
136
if initial_line == "---\n":
137
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Front matter found", sep="")
138
line = ""
139
while line != "---\n":
140
line = f.readline()
141
if line != "---\n":
142
front_matter += line
143
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Front matter loaded", sep="")
144
145
if front_matter:
146
self.front_matter = self.front_matter.load(front_matter)
147
148
print(self.front_matter, type(self.front_matter))
149
150
if "DATE" in self.front_matter:
151
self.date = parse_date_string(self.front_matter["DATE"])
152
else: # put it back
153
self.content = initial_line
154
155
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Reading content", sep="")
156
157
self.content += f.read()
158
159
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Content loaded", sep="")
160
print(colorama.Style.RESET_ALL, colorama.Style.DIM, self.content[:128] + "..." if len(self.content) > 128 else self.content)
161
except UnicodeDecodeError:
162
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, "Text decoding failed, assuming binary", sep="")
163
self.encoding = None
164
with open(file_name, "rb") as f:
165
self.content = f.read()
166
print(colorama.Style.RESET_ALL, colorama.Fore.GREEN, "Binary content loaded", sep="")
167
168
print(colorama.Style.RESET_ALL, colorama.Fore.CYAN, colorama.Style.DIM, f"Transforming URL {self.file_name} ->", end=" ", sep="")
169
self.file_name = url_transform(self.file_name)
170
print(colorama.Style.RESET_ALL, colorama.Style.BRIGHT, colorama.Fore.LIGHTYELLOW_EX, self.file_name)
171
172
print(colorama.Style.RESET_ALL, end="")
173
174
def __repr__(self):
175
return f"Document({self.file_name})"
176
177
def __str__(self):
178
return self.content
179
180
def __getitem__(self, item):
181
return self.front_matter[item]
182
183
184
class Index:
185
def __init__(self, directory, recursive=False, url_transform=lambda x: x, sort_by=lambda x: x.file_name, exclude=None):
186
self.directory = directory
187
# Temporarily move to the specified directory in order to read the files.
188
if exclude:
189
regex = re.compile(exclude)
190
else:
191
regex = re.compile("(?!)")
192
with in_directory(directory):
193
if recursive:
194
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)]
195
else:
196
self.file_names = [i for i in os.listdir() if os.path.isfile(i) and not regex.search(i)]
197
198
self.documents = sorted([Document(i, url_transform) for i in self.file_names], key=sort_by)
199
self.__current_index = 0
200
201
def __iter__(self):
202
self.__current_index = 0
203
return self
204
205
def __next__(self):
206
if self.__current_index >= len(self.documents):
207
raise StopIteration
208
else:
209
self.__current_index += 1
210
return self.documents[self.__current_index - 1]
211
212
def __repr__(self):
213
return f"Index({self.directory}): {self.documents}"
214
215
def __len__(self):
216
return len(self.documents)
217
218
219
class Site:
220
def __init__(self, build_dir, template_dir="templates"):
221
self.build_dir = build_dir
222
self.template_engine = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))
223
self.pages = {}
224
self.context = {}
225
226
def add_page(self, location, page):
227
if location.endswith("/"):
228
location += "index.html"
229
location = location.lstrip("/") # interpret it as site root, not OS root
230
self.pages[location] = page
231
232
def add_from_index(self, index, location, template, static=False, **kwargs):
233
location = location.lstrip("/") # interpret it as site root, not OS root
234
kwargs = {**self.context, **kwargs}
235
if static:
236
for document in index:
237
self.pages[os.path.join(location, document.file_name)] = Static(self, document)
238
else:
239
for document in index:
240
self.pages[os.path.join(location, document.file_name)] = Page(self, template, document, **kwargs)
241
242
def filter(self, name):
243
def decorator(func):
244
self.template_engine.filters[name] = func
245
return func
246
247
return decorator
248
249
def build(self):
250
# Clear the build directory if it exists.
251
if os.path.isdir(self.build_dir):
252
delete_directory_contents(self.build_dir)
253
for location, page in self.pages.items():
254
# Create the required directories.
255
os.makedirs(os.path.join(self.build_dir, os.path.dirname(location)), exist_ok=True)
256
if isinstance(page, str):
257
with open(os.path.join(self.build_dir, location), "w") as f:
258
f.write(page)
259
elif isinstance(page, bytes):
260
with open(os.path.join(self.build_dir, location), "wb") as f:
261
f.write(page)
262
else:
263
raise ValueError(f"{type(page)} cannot be used as a document")
264
265
266
class Page(str):
267
def __new__(cls, site, template, document=None, **kwargs):
268
kwargs = {**site.context, **kwargs}
269
return site.template_engine.get_template(template).render(document=document, **kwargs)
270
271
272
class Static(bytes):
273
def __new__(cls, site, document):
274
return document.content
275