riazmo commited on
Commit
2799618
Β·
verified Β·
1 Parent(s): d677b7e

Upload rule_engine.py

Browse files
Files changed (1) hide show
  1. core/rule_engine.py +93 -21
core/rule_engine.py CHANGED
@@ -40,6 +40,7 @@ class TypeScaleAnalysis:
40
  ratios_between_sizes: list[float]
41
  recommendation: float
42
  recommendation_name: str
 
43
 
44
  def to_dict(self) -> dict:
45
  return {
@@ -49,6 +50,7 @@ class TypeScaleAnalysis:
49
  "is_consistent": self.is_consistent,
50
  "variance": round(self.variance, 3),
51
  "sizes_px": self.sizes_px,
 
52
  "recommendation": self.recommendation,
53
  "recommendation_name": self.recommendation_name,
54
  }
@@ -381,6 +383,7 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
381
  sizes_px = sorted(set(sizes))
382
 
383
  if len(sizes_px) < 2:
 
384
  return TypeScaleAnalysis(
385
  detected_ratio=1.0,
386
  closest_standard_ratio=1.25,
@@ -391,6 +394,7 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
391
  ratios_between_sizes=[],
392
  recommendation=1.25,
393
  recommendation_name="Major Third",
 
394
  )
395
 
396
  # Calculate ratios between consecutive sizes
@@ -402,6 +406,9 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
402
  ratios.append(ratio)
403
 
404
  if not ratios:
 
 
 
405
  return TypeScaleAnalysis(
406
  detected_ratio=1.0,
407
  closest_standard_ratio=1.25,
@@ -412,6 +419,7 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
412
  ratios_between_sizes=[],
413
  recommendation=1.25,
414
  recommendation_name="Major Third",
 
415
  )
416
 
417
  # Average ratio
@@ -425,6 +433,21 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
425
  closest_scale = min(STANDARD_SCALES.keys(), key=lambda x: abs(x - avg_ratio))
426
  scale_name = STANDARD_SCALES[closest_scale]
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  # Recommendation
429
  if is_consistent and abs(avg_ratio - closest_scale) < 0.05:
430
  # Already using a standard scale
@@ -445,6 +468,7 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
445
  ratios_between_sizes=ratios,
446
  recommendation=recommendation,
447
  recommendation_name=recommendation_name,
 
448
  )
449
 
450
 
@@ -452,45 +476,48 @@ def analyze_type_scale(typography_tokens: dict) -> TypeScaleAnalysis:
452
  # ACCESSIBILITY ANALYSIS
453
  # =============================================================================
454
 
455
- def analyze_accessibility(color_tokens: dict) -> list[ColorAccessibility]:
456
  """
457
  Analyze all colors for WCAG accessibility compliance.
458
-
459
  Args:
460
  color_tokens: Dict of color tokens with value/hex
461
-
 
 
 
462
  Returns:
463
  List of ColorAccessibility results
464
  """
465
  results = []
466
-
467
  for name, token in color_tokens.items():
468
  if isinstance(token, dict):
469
  hex_color = token.get("value") or token.get("hex") or token.get("color")
470
  else:
471
  hex_color = getattr(token, "value", None)
472
-
473
  if not hex_color or not hex_color.startswith("#"):
474
  continue
475
-
476
  try:
477
  contrast_white = get_contrast_ratio(hex_color, "#ffffff")
478
  contrast_black = get_contrast_ratio(hex_color, "#000000")
479
-
480
  passes_aa_normal = contrast_white >= 4.5 or contrast_black >= 4.5
481
  passes_aa_large = contrast_white >= 3.0 or contrast_black >= 3.0
482
  passes_aaa_normal = contrast_white >= 7.0 or contrast_black >= 7.0
483
-
484
  best_text = "#ffffff" if contrast_white > contrast_black else "#000000"
485
-
486
  # Generate fix suggestion if needed
487
  suggested_fix = None
488
  suggested_fix_contrast = None
489
-
490
  if not passes_aa_normal:
491
  suggested_fix = find_aa_compliant_color(hex_color, "#ffffff", 4.5)
492
  suggested_fix_contrast = get_contrast_ratio(suggested_fix, "#ffffff")
493
-
494
  results.append(ColorAccessibility(
495
  hex_color=hex_color,
496
  name=name,
@@ -505,7 +532,36 @@ def analyze_accessibility(color_tokens: dict) -> list[ColorAccessibility]:
505
  ))
506
  except Exception:
507
  continue
508
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  return results
510
 
511
 
@@ -668,6 +724,7 @@ def run_rule_engine(
668
  radius_tokens: dict = None,
669
  shadow_tokens: dict = None,
670
  log_callback: Optional[callable] = None,
 
671
  ) -> RuleEngineResults:
672
  """
673
  Run complete rule-based analysis on design tokens.
@@ -716,23 +773,38 @@ def run_rule_engine(
716
  # ─────────────────────────────────────────────────────────────
717
  log(" β™Ώ ACCESSIBILITY CHECK (WCAG AA/AAA)")
718
  log(" " + "─" * 40)
719
- accessibility = analyze_accessibility(color_tokens)
720
 
 
 
 
721
  failures = [a for a in accessibility if not a.passes_aa_normal]
722
  passes = len(accessibility) - len(failures)
723
-
 
724
  log(f" β”œβ”€ Colors Analyzed: {len(accessibility)}")
 
725
  log(f" β”œβ”€ AA Pass: {passes} βœ…")
726
- log(f" β”œβ”€ AA Fail: {len(failures)} {'❌' if failures else 'βœ…'}")
727
-
728
- if failures:
 
729
  log(" β”‚")
730
- log(" β”‚ ⚠️ FAILING COLORS:")
731
- for i, f in enumerate(failures[:5]):
732
  fix_info = f" β†’ πŸ’‘ Fix: {f.suggested_fix} ({f.suggested_fix_contrast:.1f}:1)" if f.suggested_fix else ""
733
  log(f" β”‚ β”œβ”€ {f.name}: {f.hex_color} ({f.contrast_on_white:.1f}:1 on white){fix_info}")
734
- if len(failures) > 5:
735
- log(f" β”‚ └─ ... and {len(failures) - 5} more")
 
 
 
 
 
 
 
 
 
736
 
737
  log("")
738
 
 
40
  ratios_between_sizes: list[float]
41
  recommendation: float
42
  recommendation_name: str
43
+ base_size: float = 16.0 # Detected base/body font size
44
 
45
  def to_dict(self) -> dict:
46
  return {
 
50
  "is_consistent": self.is_consistent,
51
  "variance": round(self.variance, 3),
52
  "sizes_px": self.sizes_px,
53
+ "base_size": self.base_size,
54
  "recommendation": self.recommendation,
55
  "recommendation_name": self.recommendation_name,
56
  }
 
383
  sizes_px = sorted(set(sizes))
384
 
385
  if len(sizes_px) < 2:
386
+ base_size = sizes_px[0] if sizes_px else 16.0
387
  return TypeScaleAnalysis(
388
  detected_ratio=1.0,
389
  closest_standard_ratio=1.25,
 
394
  ratios_between_sizes=[],
395
  recommendation=1.25,
396
  recommendation_name="Major Third",
397
+ base_size=base_size,
398
  )
399
 
400
  # Calculate ratios between consecutive sizes
 
406
  ratios.append(ratio)
407
 
408
  if not ratios:
409
+ # Detect base size even if no valid ratios
410
+ base_candidates = [s for s in sizes_px if 14 <= s <= 18]
411
+ base_size = min(base_candidates, key=lambda x: abs(x - 16)) if base_candidates else (min(sizes_px, key=lambda x: abs(x - 16)) if sizes_px else 16.0)
412
  return TypeScaleAnalysis(
413
  detected_ratio=1.0,
414
  closest_standard_ratio=1.25,
 
419
  ratios_between_sizes=[],
420
  recommendation=1.25,
421
  recommendation_name="Major Third",
422
+ base_size=base_size,
423
  )
424
 
425
  # Average ratio
 
433
  closest_scale = min(STANDARD_SCALES.keys(), key=lambda x: abs(x - avg_ratio))
434
  scale_name = STANDARD_SCALES[closest_scale]
435
 
436
+ # Detect base size (closest to 16px, or 14-18px range typical for body)
437
+ # The base size is typically the most common body text size
438
+ base_candidates = [s for s in sizes_px if 14 <= s <= 18]
439
+ if base_candidates:
440
+ # Prefer 16px if present, otherwise closest to 16
441
+ if 16 in base_candidates:
442
+ base_size = 16.0
443
+ else:
444
+ base_size = min(base_candidates, key=lambda x: abs(x - 16))
445
+ elif sizes_px:
446
+ # Fallback: find size closest to 16px
447
+ base_size = min(sizes_px, key=lambda x: abs(x - 16))
448
+ else:
449
+ base_size = 16.0
450
+
451
  # Recommendation
452
  if is_consistent and abs(avg_ratio - closest_scale) < 0.05:
453
  # Already using a standard scale
 
468
  ratios_between_sizes=ratios,
469
  recommendation=recommendation,
470
  recommendation_name=recommendation_name,
471
+ base_size=base_size,
472
  )
473
 
474
 
 
476
  # ACCESSIBILITY ANALYSIS
477
  # =============================================================================
478
 
479
+ def analyze_accessibility(color_tokens: dict, fg_bg_pairs: list[dict] = None) -> list[ColorAccessibility]:
480
  """
481
  Analyze all colors for WCAG accessibility compliance.
482
+
483
  Args:
484
  color_tokens: Dict of color tokens with value/hex
485
+ fg_bg_pairs: Optional list of actual foreground/background pairs
486
+ extracted from the DOM (each dict has 'foreground',
487
+ 'background', 'element' keys).
488
+
489
  Returns:
490
  List of ColorAccessibility results
491
  """
492
  results = []
493
+
494
  for name, token in color_tokens.items():
495
  if isinstance(token, dict):
496
  hex_color = token.get("value") or token.get("hex") or token.get("color")
497
  else:
498
  hex_color = getattr(token, "value", None)
499
+
500
  if not hex_color or not hex_color.startswith("#"):
501
  continue
502
+
503
  try:
504
  contrast_white = get_contrast_ratio(hex_color, "#ffffff")
505
  contrast_black = get_contrast_ratio(hex_color, "#000000")
506
+
507
  passes_aa_normal = contrast_white >= 4.5 or contrast_black >= 4.5
508
  passes_aa_large = contrast_white >= 3.0 or contrast_black >= 3.0
509
  passes_aaa_normal = contrast_white >= 7.0 or contrast_black >= 7.0
510
+
511
  best_text = "#ffffff" if contrast_white > contrast_black else "#000000"
512
+
513
  # Generate fix suggestion if needed
514
  suggested_fix = None
515
  suggested_fix_contrast = None
516
+
517
  if not passes_aa_normal:
518
  suggested_fix = find_aa_compliant_color(hex_color, "#ffffff", 4.5)
519
  suggested_fix_contrast = get_contrast_ratio(suggested_fix, "#ffffff")
520
+
521
  results.append(ColorAccessibility(
522
  hex_color=hex_color,
523
  name=name,
 
532
  ))
533
  except Exception:
534
  continue
535
+
536
+ # --- Real foreground-background pair checks ---
537
+ if fg_bg_pairs:
538
+ for pair in fg_bg_pairs:
539
+ fg = pair.get("foreground", "")
540
+ bg = pair.get("background", "")
541
+ element = pair.get("element", "")
542
+ if not (fg.startswith("#") and bg.startswith("#")):
543
+ continue
544
+ try:
545
+ ratio = get_contrast_ratio(fg, bg)
546
+ if ratio < 4.5:
547
+ # This pair fails AA β€” record it
548
+ fix = find_aa_compliant_color(fg, bg, 4.5)
549
+ fix_contrast = get_contrast_ratio(fix, bg)
550
+ results.append(ColorAccessibility(
551
+ hex_color=fg,
552
+ name=f"fg:{fg} on bg:{bg} ({element})",
553
+ contrast_on_white=get_contrast_ratio(fg, "#ffffff"),
554
+ contrast_on_black=get_contrast_ratio(fg, "#000000"),
555
+ passes_aa_normal=False,
556
+ passes_aa_large=ratio >= 3.0,
557
+ passes_aaa_normal=False,
558
+ best_text_color="#ffffff" if get_contrast_ratio(fg, "#ffffff") > get_contrast_ratio(fg, "#000000") else "#000000",
559
+ suggested_fix=fix,
560
+ suggested_fix_contrast=fix_contrast,
561
+ ))
562
+ except Exception:
563
+ continue
564
+
565
  return results
566
 
567
 
 
724
  radius_tokens: dict = None,
725
  shadow_tokens: dict = None,
726
  log_callback: Optional[callable] = None,
727
+ fg_bg_pairs: list[dict] = None,
728
  ) -> RuleEngineResults:
729
  """
730
  Run complete rule-based analysis on design tokens.
 
773
  # ─────────────────────────────────────────────────────────────
774
  log(" β™Ώ ACCESSIBILITY CHECK (WCAG AA/AAA)")
775
  log(" " + "─" * 40)
776
+ accessibility = analyze_accessibility(color_tokens, fg_bg_pairs=fg_bg_pairs)
777
 
778
+ # Separate individual-color failures from real FG/BG pair failures
779
+ pair_failures = [a for a in accessibility if not a.passes_aa_normal and a.name.startswith("fg:")]
780
+ color_only_failures = [a for a in accessibility if not a.passes_aa_normal and not a.name.startswith("fg:")]
781
  failures = [a for a in accessibility if not a.passes_aa_normal]
782
  passes = len(accessibility) - len(failures)
783
+
784
+ pair_count = len(fg_bg_pairs) if fg_bg_pairs else 0
785
  log(f" β”œβ”€ Colors Analyzed: {len(accessibility)}")
786
+ log(f" β”œβ”€ FG/BG Pairs Checked: {pair_count}")
787
  log(f" β”œβ”€ AA Pass: {passes} βœ…")
788
+ log(f" β”œβ”€ AA Fail (color vs white/black): {len(color_only_failures)} {'❌' if color_only_failures else 'βœ…'}")
789
+ log(f" β”œβ”€ AA Fail (real FG/BG pairs): {len(pair_failures)} {'❌' if pair_failures else 'βœ…'}")
790
+
791
+ if color_only_failures:
792
  log(" β”‚")
793
+ log(" β”‚ ⚠️ FAILING COLORS (vs white/black):")
794
+ for i, f in enumerate(color_only_failures[:5]):
795
  fix_info = f" β†’ πŸ’‘ Fix: {f.suggested_fix} ({f.suggested_fix_contrast:.1f}:1)" if f.suggested_fix else ""
796
  log(f" β”‚ β”œβ”€ {f.name}: {f.hex_color} ({f.contrast_on_white:.1f}:1 on white){fix_info}")
797
+ if len(color_only_failures) > 5:
798
+ log(f" β”‚ └─ ... and {len(color_only_failures) - 5} more")
799
+
800
+ if pair_failures:
801
+ log(" β”‚")
802
+ log(" β”‚ ❌ FAILING FG/BG PAIRS (actual on-page combinations):")
803
+ for i, f in enumerate(pair_failures[:5]):
804
+ fix_info = f" β†’ πŸ’‘ Fix: {f.suggested_fix} ({f.suggested_fix_contrast:.1f}:1)" if f.suggested_fix else ""
805
+ log(f" β”‚ β”œβ”€ {f.name}{fix_info}")
806
+ if len(pair_failures) > 5:
807
+ log(f" β”‚ └─ ... and {len(pair_failures) - 5} more")
808
 
809
  log("")
810