"""
CSS Extractor Module
Extracts CSS properties from website and compares with Figma design properties
"""
import re
import json
from typing import Dict, List, Any, Tuple
from dataclasses import dataclass
@dataclass
class CSSProperty:
"""Represents a CSS property"""
name: str
value: str
element: str
selector: str
class CSSExtractor:
"""Extract and analyze CSS properties from HTML"""
# Common typography properties to track
TYPOGRAPHY_PROPERTIES = [
'font-family', 'font-size', 'font-weight', 'font-style',
'letter-spacing', 'line-height', 'text-transform', 'text-decoration'
]
# Common spacing properties
SPACING_PROPERTIES = [
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'gap', 'column-gap', 'row-gap'
]
# Common color properties
COLOR_PROPERTIES = [
'color', 'background-color', 'border-color', 'fill', 'stroke'
]
# Common sizing properties
SIZING_PROPERTIES = [
'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height'
]
# Common shadow/effect properties
EFFECT_PROPERTIES = [
'box-shadow', 'text-shadow', 'opacity', 'filter'
]
def __init__(self):
"""Initialize CSS extractor"""
self.properties: List[CSSProperty] = []
self.computed_styles: Dict[str, Dict[str, str]] = {}
def extract_from_html(self, html_content: str) -> Dict[str, Any]:
"""
Extract CSS properties from HTML content
Args:
html_content: HTML content as string
Returns:
Dictionary with extracted CSS information
"""
result = {
"typography": self._extract_typography_styles(html_content),
"spacing": self._extract_spacing_styles(html_content),
"colors": self._extract_color_styles(html_content),
"sizing": self._extract_sizing_styles(html_content),
"effects": self._extract_effect_styles(html_content),
"layout": self._extract_layout_styles(html_content)
}
return result
def _extract_typography_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract typography-related CSS"""
typography = {
"headings": {},
"body": {},
"buttons": {},
"links": {}
}
# Extract heading styles
heading_pattern = r']*style="([^"]*)"[^>]*>([^<]*)'
for match in re.finditer(heading_pattern, html_content):
styles = self._parse_style_string(match.group(1))
typography["headings"][match.group(2)] = styles
# Extract button styles
button_pattern = r''
for match in re.finditer(button_pattern, html_content):
styles = self._parse_style_string(match.group(1))
typography["buttons"][match.group(2)] = styles
return typography
def _extract_spacing_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract spacing-related CSS"""
spacing = {
"containers": {},
"components": {},
"gaps": {}
}
# Extract container padding/margin
container_pattern = r'
]*class="[^"]*container[^"]*"[^>]*style="([^"]*)"'
for match in re.finditer(container_pattern, html_content):
styles = self._parse_style_string(match.group(1))
spacing["containers"]["main"] = styles
return spacing
def _extract_color_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract color-related CSS"""
colors = {
"text": set(),
"backgrounds": set(),
"borders": set(),
"accents": set()
}
# Extract color values
color_pattern = r'(color|background-color|border-color):\s*([#\w()]+)'
for match in re.finditer(color_pattern, html_content):
prop_type = match.group(1)
color_value = match.group(2)
if prop_type == 'color':
colors["text"].add(color_value)
elif prop_type == 'background-color':
colors["backgrounds"].add(color_value)
elif prop_type == 'border-color':
colors["borders"].add(color_value)
return {k: list(v) for k, v in colors.items()}
def _extract_sizing_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract sizing-related CSS"""
sizing = {
"images": {},
"buttons": {},
"containers": {}
}
# Extract image sizes
img_pattern = r'
![]()
]*style="([^"]*)"'
for match in re.finditer(img_pattern, html_content):
styles = self._parse_style_string(match.group(1))
sizing["images"]["image"] = styles
return sizing
def _extract_layout_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract layout-related CSS"""
layout = {
"display": {},
"positioning": {},
"flex": {},
"grid": {}
}
# Extract display types
display_pattern = r'display:\s*(\w+)'
for match in re.finditer(display_pattern, html_content):
display_type = match.group(1)
if display_type not in layout["display"]:
layout["display"][display_type] = 0
layout["display"][display_type] += 1
# Extract flex properties
flex_pattern = r'flex(?:-direction|-wrap|-grow|-shrink)?:\s*([^;]+)'
for match in re.finditer(flex_pattern, html_content):
flex_value = match.group(1).strip()
if flex_value not in layout["flex"]:
layout["flex"][flex_value] = 0
layout["flex"][flex_value] += 1
return layout
def _extract_effect_styles(self, html_content: str) -> Dict[str, Any]:
"""Extract visual effect CSS"""
effects = {
"shadows": [],
"opacity": [],
"filters": []
}
# Extract box-shadow
shadow_pattern = r'box-shadow:\s*([^;]+)'
for match in re.finditer(shadow_pattern, html_content):
effects["shadows"].append(match.group(1).strip())
# Extract opacity
opacity_pattern = r'opacity:\s*([0-9.]+)'
for match in re.finditer(opacity_pattern, html_content):
effects["opacity"].append(float(match.group(1)))
return effects
def _parse_style_string(self, style_str: str) -> Dict[str, str]:
"""Parse CSS style string into dictionary"""
styles = {}
if not style_str:
return styles
for prop in style_str.split(';'):
if ':' in prop:
key, value = prop.split(':', 1)
styles[key.strip()] = value.strip()
return styles
def compare_with_figma(self, figma_properties: Dict[str, Any],
website_css: Dict[str, Any]) -> Dict[str, Any]:
"""
Compare Figma design properties with extracted CSS
Args:
figma_properties: Properties from Figma design
website_css: CSS extracted from website
Returns:
Comparison results with differences
"""
differences = {
"typography": self._compare_typography(
figma_properties.get("typography", {}),
website_css.get("typography", {})
),
"spacing": self._compare_spacing(
figma_properties.get("spacing", {}),
website_css.get("spacing", {})
),
"colors": self._compare_colors(
figma_properties.get("colors", {}),
website_css.get("colors", {})
),
"sizing": self._compare_sizing(
figma_properties.get("sizing", {}),
website_css.get("sizing", {})
),
"effects": self._compare_effects(
figma_properties.get("effects", {}),
website_css.get("effects", {})
)
}
return differences
def _compare_typography(self, figma: Dict, website: Dict) -> List[Dict[str, Any]]:
"""Compare typography properties"""
differences = []
# Compare heading styles
figma_headings = figma.get("headings", {})
website_headings = website.get("headings", {})
for heading_text, figma_styles in figma_headings.items():
website_styles = website_headings.get(heading_text, {})
# Check font-family
if figma_styles.get("font-family") != website_styles.get("font-family"):
differences.append({
"type": "font-family",
"element": heading_text,
"figma": figma_styles.get("font-family"),
"website": website_styles.get("font-family"),
"severity": "High"
})
# Check font-size
if figma_styles.get("font-size") != website_styles.get("font-size"):
differences.append({
"type": "font-size",
"element": heading_text,
"figma": figma_styles.get("font-size"),
"website": website_styles.get("font-size"),
"severity": "High"
})
# Check letter-spacing
if figma_styles.get("letter-spacing") != website_styles.get("letter-spacing"):
differences.append({
"type": "letter-spacing",
"element": heading_text,
"figma": figma_styles.get("letter-spacing"),
"website": website_styles.get("letter-spacing"),
"severity": "Medium"
})
# Check font-weight
if figma_styles.get("font-weight") != website_styles.get("font-weight"):
differences.append({
"type": "font-weight",
"element": heading_text,
"figma": figma_styles.get("font-weight"),
"website": website_styles.get("font-weight"),
"severity": "High"
})
return differences
def _compare_spacing(self, figma: Dict, website: Dict) -> List[Dict[str, Any]]:
"""Compare spacing properties"""
differences = []
figma_containers = figma.get("containers", {})
website_containers = website.get("containers", {})
for container_name, figma_styles in figma_containers.items():
website_styles = website_containers.get(container_name, {})
# Check padding
for padding_prop in ['padding', 'padding-left', 'padding-right', 'padding-top', 'padding-bottom']:
if figma_styles.get(padding_prop) != website_styles.get(padding_prop):
differences.append({
"type": padding_prop,
"element": container_name,
"figma": figma_styles.get(padding_prop),
"website": website_styles.get(padding_prop),
"severity": "Medium"
})
# Check margin
for margin_prop in ['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom']:
if figma_styles.get(margin_prop) != website_styles.get(margin_prop):
differences.append({
"type": margin_prop,
"element": container_name,
"figma": figma_styles.get(margin_prop),
"website": website_styles.get(margin_prop),
"severity": "Medium"
})
return differences
def _compare_colors(self, figma: Dict, website: Dict) -> List[Dict[str, Any]]:
"""Compare color properties"""
differences = []
figma_text_colors = set(figma.get("text", []))
website_text_colors = set(website.get("text", []))
missing_colors = figma_text_colors - website_text_colors
for color in missing_colors:
differences.append({
"type": "text-color",
"figma": color,
"website": "missing",
"severity": "Medium"
})
return differences
def _compare_sizing(self, figma: Dict, website: Dict) -> List[Dict[str, Any]]:
"""Compare sizing properties"""
differences = []
figma_images = figma.get("images", {})
website_images = website.get("images", {})
for img_name, figma_styles in figma_images.items():
website_styles = website_images.get(img_name, {})
if figma_styles.get("width") != website_styles.get("width"):
differences.append({
"type": "image-width",
"element": img_name,
"figma": figma_styles.get("width"),
"website": website_styles.get("width"),
"severity": "Medium"
})
if figma_styles.get("height") != website_styles.get("height"):
differences.append({
"type": "image-height",
"element": img_name,
"figma": figma_styles.get("height"),
"website": website_styles.get("height"),
"severity": "Medium"
})
return differences
def _compare_effects(self, figma: Dict, website: Dict) -> List[Dict[str, Any]]:
"""Compare visual effects"""
differences = []
figma_shadows = figma.get("shadows", [])
website_shadows = website.get("shadows", [])
if len(figma_shadows) != len(website_shadows):
differences.append({
"type": "shadow-count",
"figma": len(figma_shadows),
"website": len(website_shadows),
"severity": "High"
})
return differences
def extract_and_compare(figma_properties: Dict[str, Any],
website_html: str) -> Dict[str, Any]:
"""
Convenience function to extract CSS and compare with Figma
Args:
figma_properties: Properties from Figma design
website_html: HTML content of website
Returns:
Comparison results
"""
extractor = CSSExtractor()
website_css = extractor.extract_from_html(website_html)
differences = extractor.compare_with_figma(figma_properties, website_css)
return {
"website_css": website_css,
"differences": differences,
"total_differences": sum(
len(v) for v in differences.values() if isinstance(v, list)
)
}