Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| 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'<h[1-6][^>]*style="([^"]*)"[^>]*>([^<]*)</h[1-6]>' | |
| 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'<button[^>]*style="([^"]*)"[^>]*>([^<]*)</button>' | |
| 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'<div[^>]*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'<img[^>]*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) | |
| ) | |
| } | |