Build HTML site from source files.

build(opt)

Main driver.

Source code in mccole/build.py
14
15
16
17
18
19
def build(opt):
    """Main driver."""
    files = _find_files(opt)
    markdown, others = _separate_files(files)
    _handle_markdown(opt, markdown)
    _handle_others(opt, others)

construct_parser(parser)

Parse command-line arguments.

Source code in mccole/build.py
22
23
24
25
26
27
28
def construct_parser(parser):
    """Parse command-line arguments."""
    parser.add_argument("--dst", type=Path, default="docs", help="output directory")
    parser.add_argument("--src", type=Path, default=".", help="source directory")
    parser.add_argument(
        "--templates", type=Path, default="templates", help="templates directory"
    )

Handle 'b:' bibliography links.

Source code in mccole/build.py
31
32
33
34
35
36
37
38
39
def _do_bibliography_links(opt, dest, doc):
    """Handle 'b:' bibliography links."""
    for node in doc.select("a[href]"):
        if not node["href"].startswith("b:"):
            continue
        assert node["href"].count(":") == 1
        key = node["href"].split(":")[1]
        node.string = key
        node["href"] = _make_root_prefix(opt, dest) + f"bibliography/#{key}"

Handle 'g:' glossary links.

Source code in mccole/build.py
42
43
44
45
46
47
48
49
def _do_glossary_links(opt, dest, doc):
    """Handle 'g:' glossary links."""
    for node in doc.select("a[href]"):
        if not node["href"].startswith("g:"):
            continue
        assert node["href"].count(":") == 1
        key = node["href"].split(":")[1]
        node["href"] = _make_root_prefix(opt, dest) + f"glossary/#{key}"

Handle '@/' links.

Source code in mccole/build.py
52
53
54
55
56
57
58
59
60
61
62
63
64
def _do_root_links(opt, dest, doc):
    """Handle '@/' links."""
    prefix = _make_root_prefix(opt, dest)
    targets = (
        ("a[href]", "href"),
        ("img[src]", "src"),
        ("link[href]", "href"),
        ("script[src]", "src"),
    )
    for selector, attr in targets:
        for node in doc.select(selector):
            if "@/" in node[attr]:
                node[attr] = node[attr].replace("@/", prefix)

_do_title(opt, dest, doc)

Make sure title element is filled in.

Source code in mccole/build.py
67
68
69
70
71
72
73
74
def _do_title(opt, dest, doc):
    """Make sure title element is filled in."""
    if doc.title is None:
        _warn(f"{dest} does not have <title> element")
    try:
        doc.title.string = doc.h1.get_text()
    except Exception:
        _warn(f"{dest} lacks H1 heading")

_find_files(opt)

Collect all interesting files.

Source code in mccole/build.py
77
78
79
80
81
def _find_files(opt):
    """Collect all interesting files."""
    return [
        path for path in Path(opt.src).glob("**/*.*") if _is_interesting_file(opt, path)
    ]

_handle_markdown(opt, files)

Handle Markdown files.

Source code in mccole/build.py
84
85
86
87
88
89
90
def _handle_markdown(opt, files):
    """Handle Markdown files."""
    env = Environment(loader=FileSystemLoader(opt.templates))
    for source in files:
        dest = _make_output_path(opt, source)
        html = _render_markdown(opt, env, source, dest)
        dest.write_text(html)

_handle_others(opt, files)

Handle copy-only files.

Source code in mccole/build.py
93
94
95
96
97
98
def _handle_others(opt, files):
    """Handle copy-only files."""
    for source in files:
        dest = _make_output_path(opt, source)
        content = source.read_bytes()
        dest.write_bytes(content)

_is_interesting_file(opt, path)

Is this file worth copying over?

Source code in mccole/build.py
101
102
103
104
105
106
107
108
109
110
111
112
113
def _is_interesting_file(opt, path):
    """Is this file worth copying over?"""
    if not path.is_file():
        return False
    if str(path).startswith("."):
        return False
    if str(path.parent.name).startswith("."):
        return False
    if path.is_relative_to(opt.dst):
        return False
    if path.is_relative_to(opt.templates):
        return False
    return True

_make_output_path(opt, source)

Build output path.

Source code in mccole/build.py
116
117
118
119
120
121
122
123
124
def _make_output_path(opt, source):
    """Build output path."""
    if source.suffix == ".md":
        temp = source.with_suffix("").with_suffix(".html")
    else:
        temp = source
    result = opt.dst / temp.relative_to(opt.src)
    result.parent.mkdir(parents=True, exist_ok=True)
    return result

_make_root_prefix(opt, path)

Create prefix to root for path.

Source code in mccole/build.py
127
128
129
130
131
132
def _make_root_prefix(opt, path):
    """Create prefix to root for path."""
    relative = path.relative_to(opt.dst)
    depth = len(relative.parents) - 1
    assert depth >= 0
    return "./" if (depth == 0) else "../" * depth

_render_markdown(opt, env, source, dest)

Convert Markdown to HTML.

Source code in mccole/build.py
135
136
137
138
139
140
141
142
143
144
145
146
def _render_markdown(opt, env, source, dest):
    """Convert Markdown to HTML."""
    content = source.read_text()
    template = env.get_template("page.html")
    raw_html = markdown(content, extensions=MARKDOWN_EXTENSIONS)
    rendered_html = template.render(content=raw_html)

    doc = BeautifulSoup(rendered_html, "html.parser")
    for func in [_do_bibliography_links, _do_glossary_links, _do_root_links, _do_title]:
        func(opt, dest, doc)

    return str(doc)

_separate_files(files)

Divide files into categories.

Source code in mccole/build.py
149
150
151
152
153
154
155
156
157
158
def _separate_files(files):
    """Divide files into categories."""
    markdown = []
    others = []
    for path in files:
        if path.suffix == ".md":
            markdown.append(path)
        else:
            others.append(path)
    return markdown, others

_warn(msg)

Print warning.

Source code in mccole/build.py
161
162
163
def _warn(msg):
    """Print warning."""
    print(msg, file=sys.stderr)