Files
XC_VM/tools/generate-iconify-subset.py
T
Divarion_D 6f8f091f1d build(admin-ui): add iconify subset generator script
tools/generate-iconify-subset.py scans the PHP/JS/CSS source for literal
tabler-* tokens and regenerates the used-only iconify-icons.min.css from the
full iconify-icons.css. Run it after adding a new icon. Also regenerates the
subset header banner to point at the script.
2026-09-06 22:48:40 +03:00

150 lines
4.9 KiB
Python

#!/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()