This commit is contained in:
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TEMPLATES_DIR = ROOT / "app" / "templates"
|
||||
PAGES_DIR = TEMPLATES_DIR / "pages"
|
||||
BASE_FILE = TEMPLATES_DIR / "base.html"
|
||||
CSS_DIR = ROOT / "app" / "static" / "css"
|
||||
|
||||
RULE_EXTENDS = "POL001"
|
||||
RULE_INLINE_STYLE = "POL002"
|
||||
RULE_EXTRA_ASSETS = "POL003"
|
||||
RULE_HEX_OUTSIDE_TOKENS = "POL004"
|
||||
RULE_PX_SPACING = "POL005"
|
||||
RULE_BASE_ASSETS = "POL006"
|
||||
|
||||
EXTENDS_BASE_RE = re.compile(r"\{%-?\s*extends\s+\"base\.html\"\s*-?%\}")
|
||||
INLINE_STYLE_RE = re.compile(r"<style\b|style\s*=\s*\"", re.IGNORECASE)
|
||||
ASSET_LINK_RE = re.compile(r"<link[^>]+rel=\"stylesheet\"|<script[^>]+src=\"", re.IGNORECASE)
|
||||
HEX_RE = re.compile(r"#[0-9a-fA-F]{3,8}")
|
||||
PX_SPACING_RE = re.compile(
|
||||
r"(?:margin|padding|gap|row-gap|column-gap)\s*:\s*[^;]*\d+px",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def err(errors: list[str], path: Path, line_no: int, rule: str, message: str) -> None:
|
||||
rel = path.relative_to(ROOT)
|
||||
errors.append(f"{rel}:{line_no}: {rule} {message}")
|
||||
|
||||
|
||||
def check_base_assets(errors: list[str]) -> None:
|
||||
if not BASE_FILE.exists():
|
||||
errors.append(f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} Missing base.html")
|
||||
return
|
||||
|
||||
base_content = BASE_FILE.read_text(encoding="utf-8")
|
||||
css_hits = re.findall(r'/static/css/[^\"]+', base_content)
|
||||
js_hits = re.findall(r'/static/js/[^\"]+', base_content)
|
||||
|
||||
expected_css = ["/static/css/app.css?v={{ asset_version }}"]
|
||||
expected_js = ["/static/js/app.js?v={{ asset_version }}"]
|
||||
|
||||
if css_hits != expected_css:
|
||||
errors.append(
|
||||
f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} expected CSS include {expected_css}, found {css_hits}"
|
||||
)
|
||||
|
||||
if js_hits != expected_js:
|
||||
errors.append(
|
||||
f"{BASE_FILE.relative_to(ROOT)}:1: {RULE_BASE_ASSETS} expected JS include {expected_js}, found {js_hits}"
|
||||
)
|
||||
|
||||
|
||||
def check_pages_extend_base(errors: list[str]) -> None:
|
||||
page_files = sorted(PAGES_DIR.glob("*.html"))
|
||||
if not page_files:
|
||||
errors.append(f"{PAGES_DIR.relative_to(ROOT)}:1: {RULE_EXTENDS} No page templates found")
|
||||
return
|
||||
|
||||
for path in page_files:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
if not EXTENDS_BASE_RE.search(content):
|
||||
err(
|
||||
errors,
|
||||
path,
|
||||
1,
|
||||
RULE_EXTENDS,
|
||||
"page template must contain {% extends \"base.html\" %}",
|
||||
)
|
||||
|
||||
|
||||
def check_templates_inline_and_assets(errors: list[str]) -> None:
|
||||
for path in sorted(TEMPLATES_DIR.rglob("*.html")):
|
||||
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
if INLINE_STYLE_RE.search(line):
|
||||
err(errors, path, idx, RULE_INLINE_STYLE, "inline style or <style> is forbidden")
|
||||
|
||||
if path != BASE_FILE and ASSET_LINK_RE.search(line):
|
||||
err(
|
||||
errors,
|
||||
path,
|
||||
idx,
|
||||
RULE_EXTRA_ASSETS,
|
||||
"asset includes are only allowed in templates/base.html",
|
||||
)
|
||||
|
||||
|
||||
def check_css_rules(errors: list[str]) -> None:
|
||||
for path in sorted(CSS_DIR.rglob("*.css")):
|
||||
is_tokens = path.name == "tokens.css"
|
||||
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
if not is_tokens and HEX_RE.search(line):
|
||||
err(errors, path, idx, RULE_HEX_OUTSIDE_TOKENS, "hex colors are only allowed in tokens.css")
|
||||
|
||||
if not is_tokens and PX_SPACING_RE.search(line):
|
||||
err(errors, path, idx, RULE_PX_SPACING, "hard px spacing is forbidden outside tokens.css")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
check_base_assets(errors)
|
||||
check_pages_extend_base(errors)
|
||||
check_templates_inline_and_assets(errors)
|
||||
check_css_rules(errors)
|
||||
|
||||
if errors:
|
||||
print("Policy checks failed:")
|
||||
for item in errors:
|
||||
print(f"{item}")
|
||||
print("Hint: fix violations or move token values into app/static/css/tokens.css")
|
||||
return 1
|
||||
|
||||
print("Policy checks passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user