#!/usr/bin/env python3 """Regenerate the used-only iconify subset stylesheet. The full `src/Public/assets/admin/vendor/fonts/iconify-icons.css` ships ~5900 Tabler icons (2.6 MB) but the panel references only ~150 of them. This scans the PHP/JS/CSS source for literal `tabler-*` class tokens and writes a minified `iconify-icons.min.css` next to the full file, containing just the base `.ti` rule plus the icon rules that are actually used. Both admin and reseller shells link the `.min.css`. Run it after adding a NEW tabler-* icon anywhere in the source: python3 tools/generate-iconify-subset.py Paths are resolved by locating the repo's `src/` root above this file, so it works from any CWD. Only literal `tabler-foo` tokens are detected — never build icon class names dynamically (e.g. "tabler-" + name) or the subset will miss them. """ import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) # Locate the application `src/` root (repo-root/src). tools/ sits beside src/. SRC_ROOT = os.path.join(os.path.dirname(HERE), "src") if not os.path.isdir(SRC_ROOT): # Fallback: ascend until a `src/` directory is found. node = HERE while node != os.path.dirname(node): cand = os.path.join(node, "src") if os.path.isdir(cand): SRC_ROOT = cand break node = os.path.dirname(node) FONTS_DIR = os.path.join(SRC_ROOT, "Public", "assets", "admin", "vendor", "fonts") SRC = os.path.join(FONTS_DIR, "iconify-icons.css") DST = os.path.join(FONTS_DIR, "iconify-icons.min.css") # Source trees to scan for icon usage, relative to the `src/` root. SCAN_DIRS = ["Public/Views", "Public/assets/admin/js", "Public/assets/admin/xcvm", "Core", "Modules"] SCAN_EXTS = (".php", ".js", ".css") TOKEN_RE = re.compile(r"tabler-[a-z0-9\-]+") RULE_RE = re.compile(r"\.([a-zA-Z0-9_\-]+)\s*\{(.*?)\}", re.S) def collect_used_tokens(): """Return the set of literal tabler-* tokens referenced in the source.""" used = set() for rel in SCAN_DIRS: base = os.path.join(SRC_ROOT, rel) for root, _dirs, files in os.walk(base): for name in files: if not name.endswith(SCAN_EXTS) or name.startswith("iconify-icons"): continue try: text = open(os.path.join(root, name), encoding="utf-8", errors="replace").read() except OSError: continue used.update(TOKEN_RE.findall(text)) return used def minify_block(body): """Collapse whitespace in a rule body, preserving quoted url("...") data.""" out = [] in_str = False quote = "" pending = False prev = "" for c in body: if in_str: out.append(c) if c == quote: in_str = False continue if c in "\"'": in_str = True quote = c out.append(c) prev = c pending = False continue if c in " \t\r\n\f": pending = True continue if c in ":;,": pending = False out.append(c) prev = c continue if pending: if prev and prev not in ":;,(": out.append(" ") pending = False out.append(c) prev = c return "".join(out).strip() def main(): if not os.path.isfile(SRC): sys.exit("source not found: %s" % SRC) used = collect_used_tokens() css = open(SRC, encoding="utf-8").read() base = None kept = [] kept_names = set() for m in RULE_RE.finditer(css): selector, body = m.group(1), minify_block(m.group(2)) if selector == "ti": base = ".ti{" + body + "}" elif selector in used: kept.append("." + selector + "{" + body + "}") kept_names.add(selector) missing = sorted(used - kept_names - {"ti"}) if missing: # Not fatal: tokens matched in source but absent from the full css # (typos, or icons from another set). Report so they can be fixed. sys.stderr.write("warning: %d referenced icon(s) not in %s:\n %s\n" % ( len(missing), os.path.basename(SRC), ", ".join(missing))) if base is None: sys.exit("base `.ti` rule not found in source css") header = ( "/* Generated by tools/generate-iconify-subset.py — do not edit by hand.\n" " Used-only subset of iconify-icons.css. Icons: %d.\n" " Re-run the script after adding a new tabler-* icon. */\n" % len(kept) ) open(DST, "w", encoding="utf-8").write(header + base + "\n" + "".join(kept) + "\n") orig = os.path.getsize(SRC) sub = os.path.getsize(DST) print("icons kept : %d" % len(kept)) print("subset : %s" % DST) print("size : %.1f KB (-%.1f%% vs full %.1f MB)" % ( sub / 1024, 100 * (1 - sub / orig), orig / 1024 / 1024)) if __name__ == "__main__": main()