diff --git a/scripts/build-icons.py b/scripts/build-icons.py index 44d9d94..5cca93f 100644 --- a/scripts/build-icons.py +++ b/scripts/build-icons.py @@ -246,7 +246,7 @@ def normalize_url(raw: str) -> str: - White / near-white fills (#fff, #ffffff, white) are kept as-is — they act as contrast cutouts in 2-colour logos (e.g. white letter on coloured circle). Every other colour becomes currentColor. - - inline style="fill:#..." is rewritten to a fill= attribute. + - inline fill/stroke colours are normalized without discarding other styles. - Inkscape layer translate() transforms are stripped (canvas offset artefact). """ vb_match = re.search( @@ -267,28 +267,36 @@ def normalize_url(raw: str) -> str: """Keep white; turn everything else into currentColor.""" hex_val = value.lower().strip() if hex_val in ("#fff", "#ffffff", "white", "#fefefe", "#fdfdfd"): - return 'fill="#fff"' - return 'fill="currentColor"' + return "#fff" + return "currentColor" - # Rewrite inline style="fill:#rrggbb" → standalone fill= attribute. + def _normalize_inline_style(match: re.Match) -> str: + quote, declarations = match.group(1), match.group(2) + declarations = re.sub( + r'(\bfill\s*:\s*)(#[0-9a-fA-F]{3,8}|white)\b', + lambda fill: f"{fill.group(1)}{_normalized_fill(fill.group(2))}", + declarations, + flags=re.IGNORECASE, + ) + declarations = re.sub( + r'(\bstroke\s*:\s*)#[0-9a-fA-F]{3,8}\b', + lambda stroke: f"{stroke.group(1)}currentColor", + declarations, + flags=re.IGNORECASE, + ) + return f"style={quote}{declarations}{quote}" + + # Normalize supported inline colours while preserving unrelated declarations. body = re.sub( r'style\s*=\s*(["\'])(.*?)\1', - lambda m: ( - _normalized_fill(fill.group(1)) - if (fill := re.search( - r'\bfill\s*:\s*(#[0-9a-fA-F]{3,8}|white)\b', - m.group(2), - flags=re.IGNORECASE, - )) - else m.group(0) - ), + _normalize_inline_style, body, flags=re.IGNORECASE, ) # Rewrite standalone fill="#rrggbb" attributes. body = re.sub( r'\bfill\s*=\s*(["\'])(#[0-9a-fA-F]{3,8}|white)\1', - lambda m: _normalized_fill(m.group(2)), + lambda m: f'fill="{_normalized_fill(m.group(2))}"', body, flags=re.IGNORECASE, ) diff --git a/scripts/test-build-icons-quoted-attributes.py b/scripts/test-build-icons-quoted-attributes.py index 4933e9b..df5001f 100755 --- a/scripts/test-build-icons-quoted-attributes.py +++ b/scripts/test-build-icons-quoted-attributes.py @@ -4,6 +4,7 @@ from __future__ import annotations import importlib.util +import re import sys from pathlib import Path @@ -26,7 +27,7 @@ def main() -> int: build_icons = load_build_module() svg = ( "" - "" + "" "" "" ) @@ -35,10 +36,20 @@ def main() -> int: raise AssertionError(f"single-quoted viewBox was not preserved: {normalized}") if any(color in normalized for color in ("#123456", "#654321", "#abcdef", "#111111")): raise AssertionError(f"source colors escaped normalization: {normalized}") - if normalized.count('fill="currentColor"') < 2: - raise AssertionError(f"single-quoted fills were not normalized: {normalized}") - if 'stroke="currentColor"' not in normalized: - raise AssertionError(f"single-quoted strokes were not normalized: {normalized}") + path = re.search(r"]*)>", normalized) + if path is None: + raise AssertionError(f"normalized path is missing: {normalized}") + path_attributes = path.group(1) + for declaration in ("fill:currentColor", "stroke:currentColor", "opacity:.5"): + if declaration not in path_attributes: + raise AssertionError( + f"path lost or failed to normalize {declaration}: {normalized}" + ) + if ( + 'fill="currentColor"' not in normalized + or 'stroke="currentColor"' not in normalized + ): + raise AssertionError(f"standalone quoted colours were not normalized: {normalized}") print("PASS: URL SVG normalization handles single-quoted attributes") return 0