aboutsummaryrefslogtreecommitdiffstats
path: root/godocs.py
blob: 1ba8a0674b2ba8ce3b503eac9b92fca95a8d2482 (plain)
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
import re

DRY = False
LOG_REGEX = re.compile("(20[0-9]{2})-([0-9]{2})-([0-9]{2})-([A-z\\-]+)\\.md")

@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)

    @staticmethod
    def order(entries: list[Path]) -> list[Path]:
        log_entries = []
        normal_entries = []
        for e in entries:
            if LOG_REGEX.fullmatch(e.name):
                log_entries.append(e)
            else:
                normal_entries.append(e)

        return sorted(normal_entries, key=lambda v: v.name) + sorted(log_entries, reverse=True, key=lambda v: v.name)

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, Page.order(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()