1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
from collections.abc import Iterator, Callable
from pathlib import Path
from dataclasses import dataclass
import click
import os
from urllib.parse import quote
from string import capwords
import shutil
import tempfile
DRY = False
@dataclass
class Page:
title: str
url: str
entries: list[Path]
output: Path
@staticmethod
def create(source: Path, entries: list[Path], site_url: str, output_dir: Path):
title = capwords(source.stem.replace("-", " "))
url = f"{site_url}/{quote(source.stem)}"
output = output_dir / source.stem / "index.html"
return Page(title, url, entries, output)
def renderPageCallback(template_file: Path, stylesheet_url: str) -> Callable[[Page, str], None]:
""" Callback to process the provided page metadata and output the final page to the filesystem """
from jinja2 import Environment, FileSystemLoader, select_autoescape
from markdown2 import markdown
env = Environment(
loader=FileSystemLoader(template_file.parent),
autoescape=select_autoescape()
)
template = env.get_template(template_file.name)
def entries(page: Page) -> Iterator[str]:
for entry in page.entries:
with open(entry, "r") as f:
yield markdown(f.read(), extras=["fenced-code-blocks", "strike", "tables"])
def render(page: Page, site_title: str, navigation: list[dict[str, str]]) -> None:
print(f"Rendering {page.output}")
args = {"site_title": site_title, "stylesheet_url": stylesheet_url, "navigation": navigation, "entries": entries(page)}
content = template.render(args)
if DRY:
print(content)
else:
os.makedirs(page.output.parent, exist_ok=True)
with open(page.output, "w") as f:
f.write(content)
return render
def collectPages(site_root: Path, site_url: str, output_dir: Path) -> Iterator[Page]:
""" Walk the site root collecting all the entries and organizing them into pages """
for f in os.listdir(site_root):
page_path = site_root / f
# Directories are rendered as one page with multiple entries
if os.path.isdir(page_path):
yield Page.create(page_path, sorted(list(page_path.glob("**/*.md"))), site_url, output_dir)
# Single .md files are rendered as single-entry pages
elif page_path.suffix == ".md":
yield Page.create(page_path, [page_path], site_url, output_dir)
def generateSite(site_root: Path, base_url: str, output_dir: Path, render_page: Callable[[Page, str], None]) -> None:
""" Generate the site located at the specified root """
print(f"Generating site rooted at {site_root}")
site_url = f"{base_url}/{quote(site_root.stem)}"
site_dir = output_dir / site_root.stem
site_title = capwords(site_root.stem.replace("-", " "))
pages = sorted([p for p in collectPages(site_root, site_url, site_dir)], key=lambda v: v.title)
navigation = [{"text": p.title, "url": p.url} for p in pages]
for page in pages:
render_page(page, site_title, navigation)
home = Page("Home", site_url, [], site_dir / "index.html")
render_page(home, site_title, navigation)
def generateSites(config: dict[str, str]) -> None:
""" Generate all sites according to the specified config """
docs_root = Path(config["docs_root"])
output_dir = Path(config["output_dir"])
template_file = Path(config["template_file"])
css_file = Path(config["css_file"])
base_url = config["base_url"]
stylesheet_url = f"{base_url}/{css_file.name}"
render_page = renderPageCallback(template_file, stylesheet_url)
with tempfile.TemporaryDirectory() as tmpdir:
for f in os.listdir(docs_root):
site = docs_root / f
if os.path.isdir(site):
generateSite(site, base_url, Path(tmpdir), render_page)
shutil.copyfile(css_file, Path(tmpdir) / css_file.name)
if output_dir.exists():
shutil.rmtree(output_dir)
shutil.move(tmpdir, output_dir)
@click.command()
@click.argument("config_file", nargs=1, type=click.Path())
@click.option("--dry/--no-dry", default=False, help="Instead of generating the sites, print every step to stdout")
def main(config_file, dry):
global DRY
DRY = dry
with open(config_file, "rb") as f:
import tomllib
config = tomllib.load(f)
generateSites(config)
if __name__ == '__main__':
main()
|