proarrow-0.1.0.0: fix-github-links.py
#!/usr/bin/env python3
# Point every per-entity "Github" link in the generated docs at the same place as the "Source"
# link next to it.
#
# Haddock fills the --comments-entity template with the module of the page being rendered and the
# line of the enclosing declaration, so it goes wrong for instances and re-exports listed under
# another module, and for class members (which get the line of their class). The hyperlinked
# source is right, so this derives the Github URL from it: the defining module picks the file
# (under src/ or testing/), and the entity's position in docs/src/<module>.html gives the line.
#
# Usage (from proarrow/, after haddock has written docs/): fix-github-links.py DOCS GITHUB_BASE
# where GITHUB_BASE is e.g. https://github.com/sjoerdvisscher/proarrow/blob/main/proarrow/
import glob
import os
import re
import sys
docs, base = sys.argv[1], sys.argv[2]
source_dirs = ["src", "testing"]
# The anchor ids in docs/src/<module>.html and the line each one is on.
lines = {}
def anchor_line(module, anchor):
if anchor.startswith("line-"):
return int(anchor[len("line-") :])
if module not in lines:
table, current = {}, 0
with open(os.path.join(docs, "src", module + ".html"), encoding="utf-8") as f:
for m in re.finditer(r'id="([^"]+)"', f.read()):
if m.group(1).startswith("line-"):
current = int(m.group(1)[len("line-") :])
else:
table.setdefault(m.group(1), current)
lines[module] = table
return lines[module].get(anchor)
def source_file(module):
path = module.replace(".", "/") + ".hs"
for d in source_dirs:
if os.path.exists(os.path.join(d, path)):
return d + "/" + path
return None
pair = re.compile(
r'(<a href="src/([^"#]+)\.html#([^"]+)" class="link"\s*>Source</a\s*>\s*<a href=")'
+ re.escape(base)
+ r'[^"]*(" class="link")'
)
changed = unresolved = 0
for page in glob.glob(os.path.join(docs, "*.html")):
with open(page, encoding="utf-8") as f:
text = f.read()
def fix(m):
global changed, unresolved
module, anchor = m.group(2), m.group(3)
line, path = anchor_line(module, anchor), source_file(module)
if line is None or path is None:
unresolved += 1
return m.group(0)
changed += 1
return f"{m.group(1)}{base}{path}#L{line}{m.group(4)}"
new = pair.sub(fix, text)
if new != text:
with open(page, "w", encoding="utf-8") as f:
f.write(new)
print(f"fix-github-links: {changed} links set from their Source link, {unresolved} left as they were")