|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Inject a version switcher into the sidebar of an assembled multi-version site. |
| 3 | +
|
| 4 | +Usage: inject_version_switcher.py <site_dir> <branches_file> |
| 5 | +
|
| 6 | +<branches_file> is a newline-delimited list of branch names. "main" is served |
| 7 | +from the site root, every other branch from a directory of the same name. The |
| 8 | +switcher is inserted into the sphinx_rtd_theme sidebar, just above the search |
| 9 | +box, and is skipped entirely when there is only one version to choose from. |
| 10 | +""" |
| 11 | + |
| 12 | +import html |
| 13 | +import os |
| 14 | +import sys |
| 15 | + |
| 16 | +ANCHOR = '<div role="search">' |
| 17 | +MARKER = "<!-- version-switcher -->" |
| 18 | + |
| 19 | +STYLE = """ |
| 20 | +<style> |
| 21 | +.version-switcher{margin:.5em 0 .809em;text-align:left} |
| 22 | +.version-switcher label{display:block;margin-bottom:.3em;font-size:80%; |
| 23 | + text-transform:uppercase;letter-spacing:.08em;opacity:.75;color:inherit} |
| 24 | +.version-switcher select{width:100%;padding:.35em .5em;font-size:90%; |
| 25 | + border:1px solid rgba(255,255,255,.25);border-radius:2px; |
| 26 | + background-color:rgba(255,255,255,.12);color:#fcfcfc;cursor:pointer} |
| 27 | +.version-switcher select option{color:#404040;background-color:#fcfcfc} |
| 28 | +html[data-theme="dark"] .version-switcher select{border-color:#111; |
| 29 | + background-color:#141414;color:#ddd} |
| 30 | +html[data-theme="dark"] .version-switcher select option{color:#ddd; |
| 31 | + background-color:#141414} |
| 32 | +</style> |
| 33 | +""" |
| 34 | + |
| 35 | +SCRIPT = """ |
| 36 | +<script> |
| 37 | +(function(){ |
| 38 | + var sel=document.getElementById("version-switcher-select"); |
| 39 | + if(!sel){return;} |
| 40 | + sel.addEventListener("change",function(){ |
| 41 | + var base=sel.getAttribute("data-site-root")+sel.value; |
| 42 | + var> |
| 43 | + var fallback=base+"index.html"; |
| 44 | + // Keep the reader on the same page when that page exists in the chosen |
| 45 | + // version; otherwise land on that version's index. |
| 46 | + fetch(target,{method:"HEAD"}).then(function(r){ |
| 47 | + window.location.href=r.ok?target:fallback; |
| 48 | + }).catch(function(){window.location.href=fallback;}); |
| 49 | + }); |
| 50 | +})(); |
| 51 | +</script> |
| 52 | +""" |
| 53 | + |
| 54 | + |
| 55 | +def order(branches): |
| 56 | + """main first, then the rest newest-looking first (reverse alphabetical).""" |
| 57 | + rest = sorted((b for b in branches if b != "main"), reverse=True) |
| 58 | + return (["main"] if "main" in branches else []) + rest |
| 59 | + |
| 60 | + |
| 61 | +def widget(versions, current, site_root, page_path): |
| 62 | + options = [] |
| 63 | + for name, directory in versions: |
| 64 | + selected = " selected" if name == current else "" |
| 65 | + options.append( |
| 66 | + '<option value="%s"%s>%s</option>' |
| 67 | + % (html.escape(directory), selected, html.escape(name)) |
| 68 | + ) |
| 69 | + return ( |
| 70 | + '%s<div class="version-switcher">' |
| 71 | + '<label for="version-switcher-select">Version</label>' |
| 72 | + '<select id="version-switcher-select" data-site-root="%s" data-page-path="%s">' |
| 73 | + "%s</select></div>%s%s" |
| 74 | + % ( |
| 75 | + MARKER, |
| 76 | + html.escape(site_root), |
| 77 | + html.escape(page_path), |
| 78 | + "".join(options), |
| 79 | + STYLE, |
| 80 | + SCRIPT, |
| 81 | + ) |
| 82 | + ) |
| 83 | + |
| 84 | + |
| 85 | +def main(): |
| 86 | + if len(sys.argv) != 3: |
| 87 | + sys.exit("usage: inject_version_switcher.py <site_dir> <branches_file>") |
| 88 | + site_dir, branches_file = sys.argv[1], sys.argv[2] |
| 89 | + |
| 90 | + with open(branches_file) as fh: |
| 91 | + branches = [line.strip() for line in fh if line.strip()] |
| 92 | + |
| 93 | + if len(branches) < 2: |
| 94 | + print("Only %d version(s); skipping version switcher." % len(branches)) |
| 95 | + return |
| 96 | + |
| 97 | + versions = [(b, "" if b == "main" else b + "/") for b in order(branches)] |
| 98 | + # Longest directory first so "Autograph-2027/" wins over main's "". |
| 99 | + lookup = sorted(versions, key=lambda v: len(v[1]), reverse=True) |
| 100 | + |
| 101 | + injected = skipped = 0 |
| 102 | + for dirpath, _, filenames in os.walk(site_dir): |
| 103 | + for filename in filenames: |
| 104 | + if not filename.endswith(".html"): |
| 105 | + continue |
| 106 | + path = os.path.join(dirpath, filename) |
| 107 | + rel = os.path.relpath(path, site_dir).replace(os.sep, "/") |
| 108 | + |
| 109 | + current, directory = next( |
| 110 | + (v for v in lookup if rel.startswith(v[1])), (None, None) |
| 111 | + ) |
| 112 | + if current is None: |
| 113 | + continue |
| 114 | + |
| 115 | + page_path = rel[len(directory):] |
| 116 | + site_root = "../" * rel.count("/") or "./" |
| 117 | + |
| 118 | + with open(path, encoding="utf-8") as fh: |
| 119 | + doc = fh.read() |
| 120 | + if MARKER in doc or ANCHOR not in doc: |
| 121 | + skipped += 1 |
| 122 | + continue |
| 123 | + |
| 124 | + doc = doc.replace( |
| 125 | + ANCHOR, widget(versions, current, site_root, page_path) + ANCHOR, 1 |
| 126 | + ) |
| 127 | + with open(path, "w", encoding="utf-8") as fh: |
| 128 | + fh.write(doc) |
| 129 | + injected += 1 |
| 130 | + |
| 131 | + print( |
| 132 | + "Version switcher: %d page(s) updated, %d skipped, %d version(s): %s" |
| 133 | + % (injected, skipped, len(versions), ", ".join(n for n, _ in versions)) |
| 134 | + ) |
| 135 | + |
| 136 | + |
| 137 | +if __name__ == "__main__": |
| 138 | + main() |
0 commit comments