# -*- coding: utf-8 -*- """D1 §10 Python emoji gate — Windows grep silently fails to match multibyte emoji (the D0 grep-gate zero was a false negative). This scans templates by Unicode codepoint. Run from controller/: python scripts/emoji_gate.py Exit 1 if any emoji/pictograph remains in web+setup templates. """ import io, os, sys, unicodedata sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import i18n_bundle # noqa: E402 ROOTS = [ os.path.join("internal", "web", "templates"), os.path.join("internal", "setup", "templates"), ] # Codepoint ranges that count as "emoji / pictographs / dingbats" for UI-copy purposes. # Deliberately does NOT flag Hungarian letters, typographic quotes/dashes, the middle dot (·), # arrows used as affordances (→ ↗ ↻ ↑ ↓), the multiplication sign (×), or box-drawing. def is_emoji(ch): o = ord(ch) ranges = [ (0x1F300, 0x1FAFF), # Misc symbols & pictographs, emoticons, transport, supplemental, symbols-ext (0x2600, 0x26FF), # Misc symbols (☀ ⚙ ⚠ ☁ …) (0x2700, 0x27BF), # Dingbats (✅ ✂ ✈ ✏ ✓? no — see allow) (0x1F000, 0x1F0FF), # Mahjong/dominoes/cards (0xFE00, 0xFE0F), # Variation selectors (emoji presentation) (0x1F1E6, 0x1F1FF), # Regional indicators ] if any(a <= o <= b for a, b in ranges): return True return False # Dingbat codepoints that are legitimate UI glyphs (checkmarks/crosses used as plain text marks, # not emoji). We keep these OUT of the ban — they render as monochrome text, not color emoji. ALLOW = set("✓✗✔✘•●○■▶") # ✓ ✗ ✔ ✘ • ● ○ ■ ▶ def scan(path): # v0.247.0: the copy of a converted template lives in the i18n bundle -- scan every language's # expansion, so an emoji in a translation is caught where it would render. hits = [] for lang in i18n_bundle.LANGS: for h in _scan_text(i18n_bundle.read_template(path, lang)): if h not in hits: hits.append(h) return hits def _scan_text(text): hits = [] for lineno, line in enumerate(text.split("\n"), 1): for ch in line: if ch in ALLOW: continue if is_emoji(ch): try: name = unicodedata.name(ch) except ValueError: name = "U+%04X" % ord(ch) hits.append((lineno, ch, name)) return hits def _html_files(root): # R-421 (2026-09-01): AT ANY DEPTH. This was `os.listdir`, one level only. There are no # template subdirectories today, so the gate was green and correct — and would have stayed # green the moment anyone added `templates/partials/`, which is an ordinary thing to do. # Measured: a planted template in a new partials/ directory passed every listdir-based gate # and was caught by the two that already used os.walk. See AUDIT-gate-decoys-2026-09-01.md. out = [] for dirpath, _dirs, names in os.walk(root): for fn in sorted(names): if fn.endswith('.html'): out.append(os.path.join(dirpath, fn)) return sorted(out) def main(): total = 0 for root in ROOTS: for path in _html_files(root): fn = os.path.relpath(path, root) for lineno, ch, name in scan(path): total += 1 print("%s:%d %s %s" % (fn, lineno, ch, name)) if total: print("EMOJI GATE FAILED: %d emoji found" % total) sys.exit(1) print("emoji gate OK — no emoji in web/setup templates") if __name__ == "__main__": main()