|
33 | 33 |
|
34 | 34 | # Icon requirements |
35 | 35 | PREFERRED_ICON_SIZE = 16 |
| 36 | +ALLOWED_FILL_STROKE_VALUES = {"currentcolor", "none", "inherit"} |
36 | 37 |
|
37 | 38 | # URL validation |
38 | 39 | SKIP_URL_VALIDATION = os.environ.get("SKIP_URL_VALIDATION", "").lower() in ("1", "true", "yes") |
@@ -182,6 +183,48 @@ def validate_distribution_urls(distribution: dict) -> list[str]: |
182 | 183 | return errors |
183 | 184 |
|
184 | 185 |
|
| 186 | +def validate_icon_monochrome(content: str) -> list[str]: |
| 187 | + """Validate that icon uses currentColor and no hardcoded colors.""" |
| 188 | + errors = [] |
| 189 | + reported_colors = set() |
| 190 | + |
| 191 | + # Check fill attributes - must be currentColor or none |
| 192 | + fill_matches = re.findall(r'\bfill\s*=\s*["\']([^"\']+)["\']', content, re.IGNORECASE) |
| 193 | + for fill_value in fill_matches: |
| 194 | + normalized = fill_value.strip().lower() |
| 195 | + if normalized not in ALLOWED_FILL_STROKE_VALUES: |
| 196 | + errors.append(f"Icon has hardcoded fill=\"{fill_value}\" (use currentColor or none)") |
| 197 | + reported_colors.add(fill_value.strip()) |
| 198 | + |
| 199 | + # Check stroke attributes - must be currentColor or none |
| 200 | + stroke_matches = re.findall(r'\bstroke\s*=\s*["\']([^"\']+)["\']', content, re.IGNORECASE) |
| 201 | + for stroke_value in stroke_matches: |
| 202 | + normalized = stroke_value.strip().lower() |
| 203 | + if normalized not in ALLOWED_FILL_STROKE_VALUES: |
| 204 | + errors.append(f"Icon has hardcoded stroke=\"{stroke_value}\" (use currentColor or none)") |
| 205 | + reported_colors.add(stroke_value.strip()) |
| 206 | + |
| 207 | + # Check for hardcoded colors in style attributes |
| 208 | + style_matches = re.findall(r'\bstyle\s*=\s*["\']([^"\']+)["\']', content, re.IGNORECASE) |
| 209 | + for style_value in style_matches: |
| 210 | + # Check for fill/stroke with hardcoded colors in style |
| 211 | + style_fill = re.search(r'\bfill\s*:\s*([^;]+)', style_value, re.IGNORECASE) |
| 212 | + if style_fill: |
| 213 | + fill_val = style_fill.group(1).strip().lower() |
| 214 | + if fill_val not in ALLOWED_FILL_STROKE_VALUES: |
| 215 | + errors.append(f"Icon has hardcoded style fill: {style_fill.group(1).strip()}") |
| 216 | + reported_colors.add(style_fill.group(1).strip()) |
| 217 | + style_stroke = re.search(r'\bstroke\s*:\s*([^;]+)', style_value, re.IGNORECASE) |
| 218 | + if style_stroke: |
| 219 | + stroke_val = style_stroke.group(1).strip().lower() |
| 220 | + if stroke_val not in ALLOWED_FILL_STROKE_VALUES: |
| 221 | + errors.append(f"Icon has hardcoded style stroke: {style_stroke.group(1).strip()}") |
| 222 | + reported_colors.add(style_stroke.group(1).strip()) |
| 223 | + |
| 224 | + # Deduplicate errors |
| 225 | + return list(dict.fromkeys(errors)) |
| 226 | + |
| 227 | + |
185 | 228 | def validate_icon(icon_path: Path) -> list[str]: |
186 | 229 | """Validate icon.svg and return list of warnings/errors.""" |
187 | 230 | errors = [] |
@@ -216,6 +259,10 @@ def validate_icon(icon_path: Path) -> list[str]: |
216 | 259 | if vb_width != PREFERRED_ICON_SIZE or vb_height != PREFERRED_ICON_SIZE: |
217 | 260 | errors.append(f"Icon should be {PREFERRED_ICON_SIZE}x{PREFERRED_ICON_SIZE} (got {int(vb_width)}x{int(vb_height)})") |
218 | 261 |
|
| 262 | + # Validate monochrome (currentColor) usage |
| 263 | + monochrome_errors = validate_icon_monochrome(content) |
| 264 | + errors.extend(monochrome_errors) |
| 265 | + |
219 | 266 | return errors |
220 | 267 |
|
221 | 268 |
|
|
0 commit comments