Skip to content

Build

Convert Markdown to HTML.

build(opt)

Main driver.

Source code in mccole/build.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def build(opt):
    """Main driver."""

    # Setup.
    config = load_config(opt.config)
    skips = config["skips"] | {opt.out}
    env = Environment(loader=FileSystemLoader(opt.templates))

    # Find and build files.
    files = find_files(opt, skips)
    markdown, also_html, others = split_files(files)
    handle_markdown(env, opt, config, markdown)
    handle_also_html(env, opt, config, also_html)
    handle_others(env, opt, config, others)

choose_template(env, source)

Select a template.

Source code in mccole/build.py
46
47
48
49
50
def choose_template(env, source):
    """Select a template."""
    if source.name == "slides.md":
        return env.get_template("slides.html")
    return env.get_template("page.html")

Turn 'b:key' links into bibliography references.

Source code in mccole/build.py
53
54
55
56
57
def do_bibliography_links(doc, source, context):
    """Turn 'b:key' links into bibliography references."""
    for node in doc.select("a[href]"):
        if node["href"].startswith("b:"):
            node["href"] = f"@root/bibliography.html#{node['href'][2:]}"

Fix .md links in HTML.

Source code in mccole/build.py
60
61
62
63
64
65
66
def do_cross_links(doc, source, context):
    """Fix .md links in HTML."""
    for node in doc.select("a[href]"):
        if node["href"].endswith(".md"):
            node["href"] = node["href"].replace(".md", ".html").lower()
        elif Path(node["href"]).suffix in ALSO_HTML_SUFFIX:
            node["href"] = f"{node['href']}.html"

do_glossary(doc, source, context)

Turn 'g:key' links into glossary references and insert list of terms.

Source code in mccole/build.py
69
70
71
72
73
74
75
76
77
def do_glossary(doc, source, context):
    """Turn 'g:key' links into glossary references and insert list of terms."""
    seen = set()
    for node in doc.select("a[href]"):
        if node["href"].startswith("g:"):
            key = node["href"][2:]
            node["href"] = f"@root/glossary.html#{key}"
            seen.add(key)
    insert_defined_terms(doc, source, seen, context)

do_inclusion_classes(doc, source, context)

Adjust classes of file inclusions.

Source code in mccole/build.py
80
81
82
83
84
85
86
87
88
def do_inclusion_classes(doc, source, context):
    """Adjust classes of file inclusions."""
    for node in doc.select("code[data-file]"):
        inc = node["data-file"]
        if ":" in inc:
            inc = inc.split(":")[0]
        language = f"language-{Path(inc).suffix.lstrip('.')}"
        node["class"] = language
        node.parent["class"] = language

do_title(doc, source, context)

Make sure title element is filled in.

Source code in mccole/build.py
91
92
93
94
95
96
97
def do_title(doc, source, context):
    """Make sure title element is filled in."""
    try:
        doc.title.string = doc.h1.get_text()
    except Exception:
        print(f"{source} lacks H1 heading", file=sys.stderr)
        sys.exit(1)

do_root_path_prefix(doc, source, context)

Fix @root links in HTML.

Source code in mccole/build.py
100
101
102
103
104
105
106
107
108
109
110
111
112
def do_root_path_prefix(doc, source, context):
    """Fix @root links in HTML."""
    depth = len(source.parents) - 1
    prefix = "./" if (depth == 0) else "../" * depth
    targets = (
        ("a[href]", "href"),
        ("link[href]", "href"),
        ("script[src]", "src"),
    )
    for selector, attr in targets:
        for node in doc.select(selector):
            if "@root/" in node[attr]:
                node[attr] = node[attr].replace("@root/", prefix)

handle_also_html(env, opt, config, files)

Handle files that are also saved as HTML files.

Source code in mccole/build.py
115
116
117
118
119
120
121
122
123
def handle_also_html(env, opt, config, files):
    """Handle files that are also saved as HTML files."""
    for path, info in files.items():
        output_path = make_output_path(opt.out, config["renames"], path)
        write_file(output_path, info["content"])

        embedded = AS_HTML.format(path=path, content=info["content"])
        embedded = render_markdown(env, opt, path, embedded)
        write_file(Path(f"{output_path}.html"), str(embedded))

handle_markdown(env, opt, config, files)

Handle Markdown files.

Source code in mccole/build.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def handle_markdown(env, opt, config, files):
    """Handle Markdown files."""
    # Extract cross-reference keys.
    context = {
        "bibliography": find_key_defs(files, "bibliography"),
        "glossary": find_key_defs(files, "glossary"),
    }

    # Render all documents.
    for path, info in files.items():
        info["doc"] = render_markdown(env, opt, path, info["content"], context)

    # Save results.
    for path, info in files.items():
        output_path = make_output_path(opt.out, config["renames"], path)
        write_file(output_path, str(info["doc"]))

handle_others(env, opt, config, files)

Handle copy-only files.

Source code in mccole/build.py
144
145
146
147
148
def handle_others(env, opt, config, files):
    """Handle copy-only files."""
    for path, info in files.items():
        output_path = make_output_path(opt.out, config["renames"], path)
        write_file(output_path, info["content"])

insert_defined_terms(doc, source, seen, context)

Insert list of defined terms.

Source code in mccole/build.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def insert_defined_terms(doc, source, seen, context):
    """Insert list of defined terms."""
    target = doc.select("p#terms")
    if not target:
        return
    assert len(target) == 1, f"Duplicate p#terms in {source}"
    target = target[0]
    if not seen:
        target.decompose()
        return
    glossary = {key: context["glossary"][key] for key in seen}
    glossary = {k: v for k, v in sorted(glossary.items(), key=lambda item: item[1].lower())}
    target.append("Terms defined: ")
    for i, (key, term) in enumerate(glossary.items()):
        if i > 0:
            target.append(", ")
        ref = doc.new_tag("a", href=f"@root/glossary.html#{key}")
        ref.string = term
        target.append(ref)

make_output_path(output_dir, renames, source)

Build output path.

Source code in mccole/build.py
172
173
174
175
176
177
def make_output_path(output_dir, renames, source):
    """Build output path."""
    if source.name in renames:
        source = Path(source.parent, renames[source.name])
    source = Path(str(source).replace(".md", ".html"))
    return Path(output_dir, source)

parse_args(parser)

Parse command-line arguments.

Source code in mccole/build.py
180
181
182
183
184
185
186
187
def parse_args(parser):
    """Parse command-line arguments."""
    parser.add_argument("--config", type=str, default="pyproject.toml", help="optional configuration file")
    parser.add_argument("--css", type=str, help="CSS file")
    parser.add_argument("--icon", type=str, help="icon file")
    parser.add_argument("--out", type=str, default="docs", help="output directory")
    parser.add_argument("--root", type=str, default=".", help="root directory")
    parser.add_argument("--templates", type=str, default="templates", help="templates directory")

render_markdown(env, opt, source, content, context={})

Convert Markdown to HTML.

Source code in mccole/build.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def render_markdown(env, opt, source, content, context={}):
    """Convert Markdown to HTML."""
    # Generate HTML.
    template = choose_template(env, source)
    html = markdown(content, extensions=MARKDOWN_EXTENSIONS)
    html = template.render(content=html, css_file=opt.css, icon_file=opt.icon)

    # Apply transforms if always required or if context provided.
    transformers = (
        (False, do_bibliography_links),
        (False, do_cross_links),
        (False, do_glossary),
        (False, do_inclusion_classes),
        (True, do_title),
        (True, do_root_path_prefix), # must be last
    )
    doc = BeautifulSoup(html, "html.parser")
    for is_required, func in transformers:
        if context or is_required:
            func(doc, source, context)

    return doc

split_files(files)

Divide files into categories.

Source code in mccole/build.py
214
215
216
217
218
219
220
221
222
223
224
225
226
def split_files(files):
    """Divide files into categories."""
    markdown = {}
    also_html = {}
    others = {}
    for path, info in files.items():
        if path.suffix == ".md":
            markdown[path] = info
        elif path.suffix in ALSO_HTML_SUFFIX:
            also_html[path] = info
        else:
            others[path] = info
    return markdown, also_html, others