From 15c04a04508c62acc263df148af06bbe41197acf Mon Sep 17 00:00:00 2001 From: netecho Date: Sat, 17 May 2025 09:18:55 +0800 Subject: [PATCH] Add HTML validation script and CI --- .github/workflows/test.yml | 23 ++++++++++++++ README.md | 12 ++++++++ validate_html.py | 61 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 validate_html.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..814282a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + paths: + - '**/*.html' + - '**/*.py' + - '.github/workflows/**' + - 'README.md' + pull_request: + paths: + - '**/*.html' + - '**/*.py' + - '.github/workflows/**' + - 'README.md' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Validate HTML + run: python3 validate_html.py diff --git a/README.md b/README.md index 724bdbe..1b35279 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ # solarsystem + +## Running tests + +A validation script ensures `index.html` parses correctly and that any local +asset references exist. Run it with: + +```bash +python3 validate_html.py +``` + +The same script runs automatically in CI when you open a pull request or push +changes. diff --git a/validate_html.py b/validate_html.py new file mode 100644 index 0000000..c4dbb02 --- /dev/null +++ b/validate_html.py @@ -0,0 +1,61 @@ +import os +import sys +from html.parser import HTMLParser + +errors = [] + + +def check_asset(path_str: str): + if path_str.startswith("http://") or path_str.startswith("https://"): + return + file_path = os.path.join(os.path.dirname(__file__), path_str) + if not os.path.exists(file_path): + errors.append(f"Missing asset: {path_str}") + + +class Validator(HTMLParser): + void_elements = { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", + "meta", "param", "source", "track", "wbr", + } + + def __init__(self): + super().__init__() + self.stack = [] + + def handle_starttag(self, tag, attrs): + if tag not in self.void_elements: + self.stack.append(tag) + if tag in {"script", "link"}: + for attr, value in attrs: + if (tag == "script" and attr == "src") or (tag == "link" and attr == "href"): + check_asset(value) + + def handle_endtag(self, tag): + if not self.stack: + errors.append(f"Unexpected closing tag: {tag}") + return + last = self.stack.pop() + if last != tag: + errors.append(f"Mismatched tag: expected got ") + + +if __name__ == "__main__": + with open("index.html", "r", encoding="utf-8") as f: + content = f.read() + + parser = Validator() + try: + parser.feed(content) + parser.close() + except Exception as e: + errors.append(str(e)) + + if parser.stack: + errors.append("Unclosed tags: " + ", ".join(parser.stack)) + + if errors: + for e in errors: + print(e, file=sys.stderr) + sys.exit(1) + print("index.html looks valid and all assets exist.")