postprocess_stagit_site.py (6517B)
1 #!/usr/bin/env python3 2 import re 3 import sys 4 from pathlib import Path 5 6 7 TOPBAR = """ 8 <div class="topbar"> 9 <div class="wrap"> 10 <a class="brand repo-link" href="/"><span class="mark" aria-hidden="true"><svg viewBox="0 0 32 32" focusable="false"><circle cx="9.4" cy="7.6" r="2.8" fill="currentColor"></circle><path d="M9.4 13v7.1c0 3.7 2.9 6.4 6.6 6.4s6.6-2.7 6.6-6.4V13" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"></path></svg></span><span>iuna</span></a> 11 <nav aria-label="Page sections"> 12 <a href="/#whatisiuna">What is iuna</a> 13 <a href="/#howtojoin">Join</a> 14 <a href="/git/iuna/file/docs/protocol.md.html">Protocol</a> 15 <a href="/git/iuna/file/README.md.html">Source</a> 16 <a href="/downloads/">Downloads</a> 17 </nav> 18 </div> 19 </div>""" 20 21 22 LINE_RE = re.compile(r'(?P<line><a\s+[^>]*class="line"[^>]*>.*?</a>)(?P<body>.*)', re.S) 23 BLOB_RE = re.compile(r'(<pre id="blob"[^>]*>)(.*?)(</pre>)', re.S) 24 25 26 def postprocess_html(html: str, path: Path | None = None) -> str: 27 if '<div class="topbar">' not in html: 28 html = re.sub(r"<body([^>]*)>", r"<body\1>\n" + TOPBAR, html, count=1) 29 30 html = re.sub( 31 r"<td id=\"logo\"[^>]*>.*?</td>", 32 "", 33 html, 34 count=1, 35 flags=re.S, 36 ) 37 html = re.sub( 38 r"<td><a href=\"[^\"]*\"><img src=\"[^\"]*logo\.png\"[^>]*></a></td>", 39 "", 40 html, 41 count=1, 42 flags=re.S, 43 ) 44 html = re.sub(r"<tr([^>]*)><td></td><td>", r"<tr\1><td>", html) 45 html = style_clone_command(html) 46 47 if path is not None and is_markdown_file_page(path): 48 html = style_markdown_blob(html) 49 return html 50 51 52 def style_clone_command(html: str) -> str: 53 return re.sub( 54 r'git clone\s+<a\s+href="[^"]+"[^>]*>(.*?)</a>', 55 r'<code class="clone-command">git clone \1</code>', 56 html, 57 flags=re.S, 58 ) 59 60 61 def is_markdown_file_page(path: Path) -> bool: 62 return path.name.endswith(".md.html") or path.name.endswith(".markdown.html") 63 64 65 def style_markdown_blob(html: str) -> str: 66 def replace_blob(match: re.Match[str]) -> str: 67 opening, blob, closing = match.groups() 68 if re.search(r'class="[^"]*\bmd-source\b', opening): 69 return match.group(0) 70 if 'class="' in opening: 71 opening = re.sub(r'class="([^"]*)"', r'class="\1 md-source"', opening, count=1) 72 else: 73 opening = opening[:-1] + ' class="md-source">' 74 return opening + style_markdown_lines(blob) + closing 75 76 return BLOB_RE.sub(replace_blob, html, count=1) 77 78 79 def style_markdown_lines(blob: str) -> str: 80 lines = blob.splitlines(keepends=True) 81 in_fence = False 82 styled = [] 83 for line in lines: 84 newline = "" 85 if line.endswith("\r\n"): 86 line, newline = line[:-2], "\r\n" 87 elif line.endswith("\n"): 88 line, newline = line[:-1], "\n" 89 90 match = LINE_RE.match(line) 91 if not match: 92 styled.append(line + newline) 93 continue 94 95 anchor = match.group("line") 96 body = match.group("body") 97 stripped = body.lstrip(" ") 98 leading = body[: len(body) - len(stripped)] 99 if stripped.startswith("```") or stripped.startswith("~~~"): 100 in_fence = not in_fence 101 body = leading + '<span class="md-fence">' + stripped + "</span>" 102 elif not in_fence: 103 body = style_markdown_line_body(body) 104 styled.append(anchor + body + newline) 105 return "".join(styled) 106 107 108 def style_markdown_line_body(body: str) -> str: 109 if not body.strip(): 110 return body 111 112 leading = body[: len(body) - len(body.lstrip(" "))] 113 text = body[len(leading) :] 114 heading = re.match(r"(#{1,6})(\s+)(.*)", text) 115 if heading: 116 level = len(heading.group(1)) 117 return ( 118 leading 119 + f'<span class="md-heading md-heading-{level}">' 120 + '<span class="md-marker">' 121 + heading.group(1) 122 + heading.group(2) 123 + "</span>" 124 + style_markdown_inline(heading.group(3)) 125 + "</span>" 126 ) 127 128 blockquote = re.match(r"(>)(\s?)(.*)", text) 129 if blockquote: 130 return ( 131 leading 132 + '<span class="md-blockquote"><span class="md-marker">' 133 + blockquote.group(1) 134 + blockquote.group(2) 135 + "</span>" 136 + style_markdown_inline(blockquote.group(3)) 137 + "</span>" 138 ) 139 140 unordered = re.match(r"([-*+])(\s+)(.*)", text) 141 if unordered: 142 return leading + style_markdown_marker_line("md-list", unordered) 143 144 ordered = re.match(r"(\d+\.)(\s+)(.*)", text) 145 if ordered: 146 return leading + style_markdown_marker_line("md-list", ordered) 147 148 thematic_break = re.match(r"((?:[-*_]\s*){3,})$", text) 149 if thematic_break: 150 return leading + '<span class="md-rule">' + thematic_break.group(1) + "</span>" 151 152 return leading + style_markdown_inline(text) 153 154 155 def style_markdown_marker_line(class_name: str, match: re.Match[str]) -> str: 156 return ( 157 f'<span class="{class_name}"><span class="md-marker">' 158 + match.group(1) 159 + match.group(2) 160 + "</span>" 161 + style_markdown_inline(match.group(3)) 162 + "</span>" 163 ) 164 165 166 def style_markdown_inline(text: str) -> str: 167 spans: list[str] = [] 168 169 def stash(class_name: str, value: str) -> str: 170 spans.append(f'<span class="{class_name}">{value}</span>') 171 return f"\0{len(spans) - 1}\0" 172 173 text = re.sub(r"`([^`\n]+)`", lambda m: stash("md-code", m.group(1)), text) 174 text = re.sub(r"\*\*([^*\n]+)\*\*", r"<strong>\1</strong>", text) 175 text = re.sub(r"__([^_\n]+)__", r"<strong>\1</strong>", text) 176 text = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", text) 177 178 def restore(match: re.Match[str]) -> str: 179 return spans[int(match.group(1))] 180 181 return re.sub(r"\0(\d+)\0", restore, text) 182 183 184 def main() -> int: 185 if len(sys.argv) != 2: 186 print(f"Usage: {sys.argv[0]} <generated-git-root>", file=sys.stderr) 187 return 2 188 189 git_root = Path(sys.argv[1]) 190 if not git_root.is_dir(): 191 print(f"error: generated git root does not exist: {git_root}", file=sys.stderr) 192 return 1 193 194 for path in git_root.rglob("*.html"): 195 html = path.read_text(encoding="utf-8") 196 path.write_text(postprocess_html(html, path), encoding="utf-8") 197 198 return 0 199 200 201 if __name__ == "__main__": 202 raise SystemExit(main())