""" 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']*style="([^"]*)"[^>]*>([^<]*)' 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) ) }