This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user