iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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"(&gt;)(\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())