Files
felhom.eu/scripts/site_gates.py
T

134 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""TASK-D3 site gates — mechanical checks against the static site's known failure mode:
silent per-page drift. Run from the repo root: python scripts/site_gates.py
Gates (all must pass; non-zero exit on any failure):
1. BOM — every website/*.html begins with EF BB BF (byte-checked)
2. emoji — zero emoji/pictographs in website/*.html (codepoint ranges; NEVER grep —
Windows grep false-negatives multibyte emoji, proven in D0)
3. nav — the <nav>…</nav> and <footer>…</footer> blocks of all pages are identical
after stripping the active-link marker
4. analytics — the umami snippet is present on every public page (nonpublic draft exempt)
5. no-CDN — zero fonts.googleapis.com / fonts.gstatic.com references
6. banned — zero legacy hexes / 999px radius / box-shadow in pages + site.css
7. style — zero embedded <style> blocks in pages (everything lives in site.css)
8. cachebust — every site.css / icons.svg reference carries ?v=<N>
"""
import io, os, re, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
W = os.path.join(ROOT, "website")
PAGES = ["index.html", "kapcsolat.html", "alkalmazasok.html", "technologiak.html",
"biztonsagimentes.html", "gyik.html", "szolgaltatasok-nonpublic.html",
"letoltes.html"]
ANALYTICS_EXEMPT = {"szolgaltatasok-nonpublic.html"}
ANALYTICS_MARK = "https://stats.felhom.eu/script.js"
BANNED = ["fonts.googleapis.com", "fonts.gstatic.com",
"#0d1117", "#161b22", "#1c2128", "#30363d", "#238636", "#da3633", "#d29922",
"border-radius: 999px", "box-shadow"]
fails = []
def fail(msg):
fails.append(msg)
print("FAIL:", msg)
def is_emoji(ch):
o = ord(ch)
return any(a <= o <= b for a, b in [
(0x1F000, 0x1FAFF), # pictographs, emoticons, transport, symbols-ext, cards
(0x2600, 0x27BF), # misc symbols + dingbats (incl. ✓ ✗ ★ ⚠ — sprite icons instead)
(0x2300, 0x23FF), # misc technical (⏱ ⏳ ⌛ …)
(0x2B00, 0x2BFF), # ⭐ etc.
(0xFE00, 0xFE0F), # variation selectors
(0x1F1E6, 0x1F1FF), # regional indicators
])
def norm_block(b):
b = b.replace(' class="active"', "")
b = re.sub(r'\s*aria-current="[^"]*"', "", b)
return b
pages = {}
for p in PAGES:
path = os.path.join(W, p)
raw = io.open(path, "rb").read()
# gate 1: BOM
if raw[:3] != b"\xef\xbb\xbf":
fail("%s: missing UTF-8 BOM (first bytes: %s)" % (p, raw[:3].hex()))
pages[p] = raw.decode("utf-8-sig")
# gate 2: emoji (pages + the shared stylesheet)
total_emoji = 0
emoji_targets = dict(pages)
_css = os.path.join(W, "assets", "site.css")
if os.path.exists(_css):
emoji_targets["assets/site.css"] = io.open(_css, encoding="utf-8").read()
for p, s in emoji_targets.items():
hits = [(i, ch) for i, ch in enumerate(s) if is_emoji(ch)]
if hits:
total_emoji += len(hits)
sample = " ".join(ch for _, ch in hits[:10])
fail("%s: %d emoji (%s ...)" % (p, len(hits), sample))
if total_emoji:
print(" emoji total: %d" % total_emoji)
# gate 3: nav + footer consistency
ref_nav = ref_footer = None
for p, s in pages.items():
nm = re.search(r"<nav>.*?</nav>", s, re.DOTALL)
fm = re.search(r"<footer>.*?</footer>", s, re.DOTALL)
if not nm or not fm:
fail("%s: missing <nav> or <footer>" % p)
continue
nav, foot = norm_block(nm.group(0)), norm_block(fm.group(0))
if ref_nav is None:
ref_nav, ref_footer, ref_page = nav, foot, p
else:
if nav != ref_nav:
fail("%s: <nav> differs from %s (after active-marker normalization)" % (p, ref_page))
if foot != ref_footer:
fail("%s: <footer> differs from %s" % (p, ref_page))
# gate 4: analytics presence
for p, s in pages.items():
if p in ANALYTICS_EXEMPT:
continue
if ANALYTICS_MARK not in s:
fail("%s: analytics snippet missing (%s)" % (p, ANALYTICS_MARK))
# gate 5+6: banned strings in pages + site.css
targets = dict(pages)
css_path = os.path.join(W, "assets", "site.css")
if os.path.exists(css_path):
targets["assets/site.css"] = io.open(css_path, encoding="utf-8").read()
for name, s in targets.items():
low = s.lower()
for pat in BANNED:
c = low.count(pat.lower())
if c:
fail("%s: banned %r ×%d" % (name, pat, c))
# gate 7: no embedded style blocks
for p, s in pages.items():
c = len(re.findall(r"<style[\s>]", s))
if c:
fail("%s: %d embedded <style> block(s)" % (p, c))
# gate 8: cache-busting on shared assets
for p, s in pages.items():
for m in re.finditer(r"/assets/(site\.css|icons\.svg)([^\"'#\s>]*)", s):
if not m.group(2).startswith("?v="):
fail("%s: %s referenced without ?v= (got %r)" % (p, m.group(1), m.group(0)))
if fails:
print("\nSITE GATES FAILED: %d problem(s)" % len(fails))
sys.exit(1)
print("site gates OK — BOM, emoji=0, nav/footer consistent, analytics present, no CDN, no legacy tokens, no <style>, cache-busted assets")