diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65f74cc..5cbecae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: - name: Verify accessible SVG contract run: python3 scripts/test-lint-a11y.py + - name: Verify Devicon normalization + run: python3 scripts/test-build-icons-devicon.py + - name: Run skin linter run: python3 scripts/lint-skin.py --all --baseline diff --git a/scripts/build-icons.py b/scripts/build-icons.py index b191b2a..e78045b 100644 --- a/scripts/build-icons.py +++ b/scripts/build-icons.py @@ -292,6 +292,10 @@ def normalize_url(raw: str) -> str: def normalize_devicon(raw: str) -> str: """Devicon SVGs have colored fills and a 128x128 or 32x32 viewBox. Strip fills/classes and rewrite to 24x24 currentColor.""" + viewbox_match = re.search( + r'\bviewBox\s*=\s*(["\'])(.*?)\1', raw, flags=re.IGNORECASE + ) + viewbox = viewbox_match.group(2).strip() if viewbox_match else "0 0 128 128" inner = re.search(r"]*>(.*?)", raw, flags=re.DOTALL | re.IGNORECASE) if not inner: raise ValueError("no in Devicon payload") @@ -303,7 +307,7 @@ def normalize_devicon(raw: str) -> str: body = re.sub(r'\bclass="[^"]*"', 'fill="currentColor"', body) body = re.sub(r"\s+", " ", body).strip() return ( - '' ) diff --git a/scripts/test-build-icons-devicon.py b/scripts/test-build-icons-devicon.py new file mode 100755 index 0000000..ecb57e5 --- /dev/null +++ b/scripts/test-build-icons-devicon.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Regression tests for Devicon viewBox preservation.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +BUILD = ROOT / "scripts" / "build-icons.py" + + +def load_build_module(): + sys.dont_write_bytecode = True + spec = importlib.util.spec_from_file_location("diagram_design_build_icons", BUILD) + if spec is None or spec.loader is None: + raise AssertionError("could not load build-icons.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main() -> int: + build_icons = load_build_module() + svg = '' + normalized = build_icons.normalize_devicon(svg) + if 'viewBox="0 0 32 32"' not in normalized: + raise AssertionError(f"source viewBox was not preserved: {normalized}") + if 'fill="#123456"' in normalized: + raise AssertionError("source fill escaped currentColor normalization") + if 'fill="currentColor"' not in normalized: + raise AssertionError("normalized icon does not inherit currentColor") + print("PASS: Devicon normalization preserves non-128 source viewBoxes") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())