98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail when the static site contains broken or non-portable local references."""
|
|
|
|
from html.parser import HTMLParser
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SKIP_PREFIXES = ("#", "data:", "mailto:", "tel:", "javascript:")
|
|
|
|
|
|
class PageParser(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.references = []
|
|
self.ids = set()
|
|
self.has_viewport = False
|
|
self.has_csp = False
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
values = dict(attrs)
|
|
element_id = values.get("id")
|
|
if element_id:
|
|
self.ids.add(element_id)
|
|
if tag == "meta" and values.get("name", "").lower() == "viewport":
|
|
self.has_viewport = True
|
|
if tag == "meta" and values.get("http-equiv", "").lower() == "content-security-policy":
|
|
self.has_csp = True
|
|
for attribute in ("href", "src", "action"):
|
|
value = values.get(attribute)
|
|
if value:
|
|
self.references.append((tag, attribute, value))
|
|
|
|
|
|
def parse_page(path):
|
|
parser = PageParser()
|
|
parser.feed(path.read_text(encoding="utf-8"))
|
|
return parser
|
|
|
|
|
|
def main():
|
|
pages = sorted(ROOT.glob("*.html"))
|
|
parsed = {page.resolve(): parse_page(page) for page in pages}
|
|
errors = []
|
|
|
|
if not (ROOT / "index.html").is_file():
|
|
errors.append("missing GitHub Pages entry point: index.html")
|
|
|
|
for page in pages:
|
|
parser = parsed[page.resolve()]
|
|
if not parser.has_viewport:
|
|
errors.append(f"{page.name}: missing viewport metadata")
|
|
if not parser.has_csp:
|
|
errors.append(f"{page.name}: missing Content Security Policy")
|
|
|
|
for tag, attribute, raw_value in parser.references:
|
|
value = raw_value.strip()
|
|
if not value or value.startswith(SKIP_PREFIXES):
|
|
continue
|
|
|
|
url = urlsplit(value)
|
|
if url.scheme or url.netloc:
|
|
errors.append(f"{page.name}: remote dependency in {attribute}: {value}")
|
|
continue
|
|
if url.path.startswith("/"):
|
|
errors.append(f"{page.name}: root-absolute path breaks project Pages: {value}")
|
|
continue
|
|
|
|
target = (page.parent / unquote(url.path or page.name)).resolve()
|
|
try:
|
|
target.relative_to(ROOT)
|
|
except ValueError:
|
|
errors.append(f"{page.name}: reference leaves repository: {value}")
|
|
continue
|
|
if not target.exists():
|
|
errors.append(f"{page.name}: missing {tag} {attribute} target: {value}")
|
|
continue
|
|
|
|
if url.fragment and target.suffix.lower() == ".html":
|
|
target_parser = parsed.get(target)
|
|
if target_parser is None:
|
|
target_parser = parse_page(target)
|
|
parsed[target] = target_parser
|
|
if unquote(url.fragment) not in target_parser.ids:
|
|
errors.append(f"{page.name}: missing anchor target: {value}")
|
|
|
|
if errors:
|
|
print("Site validation failed:")
|
|
for error in errors:
|
|
print(f"- {error}")
|
|
raise SystemExit(1)
|
|
|
|
print(f"Site validation passed: {len(pages)} HTML pages checked.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|