dotfiles
My PC Dotfiles & Websitegit clone https://github.com/maydayv7/dotfiles
site/git/assemble.py (6850B)
1"""Apply website theme to gitsite"""
2
3import argparse
4from copy import deepcopy
5from pathlib import Path
6import posixpath
7import shutil
8import subprocess
9from urllib.parse import quote, unquote, urlsplit
10
11from lxml import html
12
13
14def serialize(node):
15 return html.tostring(node, encoding="unicode", method="html")
16
17
18def export(source, output, base_url):
19 output.mkdir(parents=True, exist_ok=True)
20 shell = html.parse(str(source / "git-shell/index.html"))
21 prefix = base_url.rstrip("/") + "/"
22
23 # Local Assets
24 for node in shell.xpath("//*[@src or @srcset] | //link[@href]"):
25 for attr in ("src", "href", "srcset"):
26 if node.get(attr):
27 node.set(attr, node.get(attr).replace(prefix, "/"))
28
29 shell.find("body").set("class", "git-site")
30 (output / "shell.html").write_text(serialize(shell), encoding="utf-8")
31
32 for asset in (
33 "style.css",
34 "git.css",
35 "fonts.css",
36 "color.css",
37 "fonts",
38 "website.ico",
39 "math",
40 "js",
41 "processed_images",
42 ):
43 src, dst = source / asset, output / asset
44 if src.is_dir():
45 shutil.copytree(src, dst)
46 else:
47 shutil.copyfile(src, dst)
48
49
50def rewrite_markdown(article, page, output, repo):
51 """Resolve Markdown links and images"""
52 relative = page.relative_to(output)
53 if len(relative.parts) < 3 or relative.parts[1] != "file":
54 return
55 repo_name = relative.parts[0]
56 filename = "/".join(relative.parts[2:])[:-5] # strip stagit's .html suffix
57 for node in article.xpath(".//*[@href or @src]"):
58 attr = "src" if node.tag == "img" else "href"
59 value = node.get(attr)
60 if not value:
61 continue
62
63 url = urlsplit(value)
64 if url.scheme or url.netloc or not url.path:
65 continue
66
67 decoded = unquote(url.path).replace("\\", "/")
68 target = posixpath.normpath(
69 decoded.lstrip("/")
70 if decoded.startswith("/")
71 else posixpath.join(posixpath.dirname(filename), decoded)
72 )
73 if target == ".." or target.startswith(("../", "/")) or "\0" in target:
74 node.attrib.pop(attr, None)
75 continue
76
77 suffix = ("?" + url.query if url.query else "") + (
78 "#" + url.fragment if url.fragment else ""
79 )
80 if attr == "src":
81 if Path(target).suffix.lower() not in {
82 ".png",
83 ".jpg",
84 ".jpeg",
85 ".gif",
86 ".webp",
87 ".avif",
88 ".ico",
89 ".svg",
90 }:
91 node.attrib.pop(attr, None)
92 continue
93 dest = output / repo_name / "raw" / target
94 if not dest.is_file():
95 blob = subprocess.run(
96 ["git", "--git-dir", str(repo), "show", f"HEAD:{target}"],
97 capture_output=True,
98 )
99 if blob.returncode:
100 node.attrib.pop(attr, None)
101 continue
102 dest.parent.mkdir(parents=True, exist_ok=True)
103 dest.write_bytes(blob.stdout)
104 node.set(attr, f"/{quote(repo_name)}/raw/{quote(target)}{suffix}")
105
106 elif attr == "href":
107 dest = output / repo_name / "file" / (target + ".html")
108 if dest.is_file():
109 node.set(attr, f"/{quote(repo_name)}/file/{quote(target)}.html{suffix}")
110 else:
111 # Directory Links
112 directory = output / repo_name / "file" / target
113 if target == "." or directory.is_dir():
114 node.set(attr, f"/{quote(repo_name)}/files.html")
115
116
117def assemble(output, assets, repositories):
118 shell = html.parse(str(assets / "shell.html"))
119 for path in sorted(output.rglob("*.html")):
120 original = html.parse(str(path))
121 main = original.find("body/main")
122 if main is None:
123 raise ValueError(f"Missing <main> in {path}")
124
125 document = deepcopy(shell)
126 document.find("head/title").text = original.findtext("head/title")
127 for feed in original.xpath("//head/link[@rel='alternate']"):
128 document.find("head").append(deepcopy(feed))
129 placeholder = document.find(".//main")
130 if placeholder is None:
131 raise ValueError("Missing <main> in git-shell.html")
132 placeholder.getparent().replace(placeholder, main)
133
134 for article in main.iter("article"):
135 name = path.relative_to(output).parts[0]
136 rewrite_markdown(article, path, output, repositories / f"{name}.git")
137
138 for alert in main.xpath(
139 ".//*[contains(concat(' ', @class, ' '), ' admonition ')]"
140 ):
141 kind = next(
142 (
143 value
144 for value in alert.get("class").split()
145 if value != "admonition"
146 ),
147 "note",
148 )
149 alert.tag = "blockquote"
150 alert.set(
151 "class", "markdown-alert-" + {"danger": "caution"}.get(kind, kind)
152 )
153
154 for table in main.xpath(".//table[not(ancestor::table)]"):
155 for cell in table.xpath("./thead/tr/td"):
156 cell.tag = "th"
157 cell.set("scope", "col")
158 wrapper = html.Element(
159 "div",
160 {
161 "class": "table-scroll",
162 "tabindex": "0",
163 "role": "region",
164 "aria-label": "Repository table",
165 },
166 )
167 table.addprevious(wrapper)
168 wrapper.append(table)
169
170 for pre in main.xpath(".//pre"):
171 pre.set("tabindex", "0")
172
173 # Math
174 if main.xpath(".//*[contains(concat(' ', @class, ' '), ' arithmatex ')]"):
175 document.find("body").append(
176 html.Element("script", defer="", src="/js/git-math.js")
177 )
178
179 path.write_text("<!doctype html>\n" + serialize(document), encoding="utf-8")
180
181
182if __name__ == "__main__":
183 parser = argparse.ArgumentParser(description=__doc__)
184 parser.add_argument("command", choices=["export", "assemble"])
185 parser.add_argument("source", type=Path)
186 parser.add_argument("destination", type=Path)
187 parser.add_argument("--repositories", type=Path)
188 parser.add_argument("--base-url")
189
190 args = parser.parse_args()
191 if args.command == "export":
192 if not args.base_url:
193 parser.error("export requires --base-url")
194 export(args.source, args.destination, args.base_url)
195 else:
196 if not args.repositories:
197 parser.error("assemble requires --repositories")
198 assemble(args.source, args.destination, args.repositories)