AkashKumarave commited on
Commit
883f8b8
·
verified ·
1 Parent(s): 443869b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +517 -259
app.py CHANGED
@@ -17,75 +17,11 @@ app = Flask(__name__)
17
  # Setup logging
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
 
20
 
21
  @app.route('/')
22
  def home():
23
- return '''
24
- <html>
25
- <head>
26
- <title>Website to Figma API</title>
27
- <style>
28
- body {
29
- font-family: Arial, sans-serif;
30
- max-width: 800px;
31
- margin: 0 auto;
32
- padding: 20px;
33
- line-height: 1.6;
34
- }
35
- h1 {
36
- color: #333;
37
- }
38
- pre {
39
- background-color: #f5f5f5;
40
- padding: 10px;
41
- border-radius: 5px;
42
- overflow-x: auto;
43
- }
44
- </style>
45
- </head>
46
- <body>
47
- <h1>Website to Figma Converter API</h1>
48
- <p>This API converts websites into Figma-compatible data structures.</p>
49
-
50
- <h2>API Endpoints:</h2>
51
- <h3>POST /api/convert</h3>
52
- <p>Converts a website to Figma elements.</p>
53
-
54
- <h4>Request Body:</h4>
55
- <pre>
56
- {
57
- "url": "https://example.com",
58
- "viewport_width": 1440 // Optional, defaults to 1440px
59
- }
60
- </pre>
61
-
62
- <h4>Response:</h4>
63
- <pre>
64
- {
65
- "status": "success",
66
- "viewport_width": 1440,
67
- "viewport_height": 900,
68
- "elements": [
69
- {
70
- "type": "rectangle",
71
- "x": 0,
72
- "y": 0,
73
- "width": 200,
74
- "height": 100,
75
- "style": {
76
- "backgroundColor": "#ffffff",
77
- "borderRadius": "5px"
78
- }
79
- },
80
- // more elements...
81
- ]
82
- }
83
- </pre>
84
-
85
- <p>This API is part of the Website to Figma plugin.</p>
86
- </body>
87
- </html>
88
- '''
89
 
90
  @app.route('/api/convert', methods=['POST'])
91
  def convert_website():
@@ -115,22 +51,21 @@ def convert_website():
115
  'Accept-Language': 'en-US,en;q=0.9',
116
  }
117
 
118
- response = requests.get(url, headers=headers, timeout=15)
119
  response.raise_for_status() # Raise an exception for HTTP errors
120
 
121
- # Parse the HTML content
122
- soup = BeautifulSoup(response.text, 'html.parser')
123
 
124
- # Extract the page elements using BeautifulSoup
125
- elements = extract_elements_bs(soup)
126
 
127
- # Extract CSS styles
128
- styles = extract_styles_bs(soup, url)
129
 
130
- # Apply styles to elements
131
- apply_styles_to_elements(elements, styles)
132
 
133
- # Estimate page height based on content (simplified approach)
134
  estimated_height = viewport_height
135
  if elements:
136
  # Find the maximum y-coordinate plus height
@@ -160,243 +95,566 @@ def convert_website():
160
  logger.error(traceback.format_exc())
161
  return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500
162
 
163
- def extract_elements_bs(soup):
164
- """Extract elements from the webpage using BeautifulSoup"""
165
- elements = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # Helper function to get the coordinates and dimensions
168
- # For simplicity, we'll just stack elements vertically
169
- y_position = 0
170
- max_width = 1440 # Default max width
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- # Process body and its children
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  body = soup.find('body')
174
  if not body:
175
  return elements
176
 
177
- # Add the body as a container
178
- body_element = {
179
- 'type': 'container',
180
- 'tagName': 'body',
181
- 'x': 0,
182
- 'y': 0,
183
- 'width': max_width,
184
- 'height': 900, # Default height
185
- 'children': []
186
- }
187
 
188
- # Process main content elements
189
- for element in body.find_all(['div', 'header', 'main', 'footer', 'section', 'nav'], recursive=False):
190
- element_data = process_element(element, 0, y_position, max_width)
191
- if element_data:
192
- y_position += element_data['height'] + 10 # Add spacing
193
- body_element['children'].append(element_data)
194
 
195
- # Adjust body height
196
- body_element['height'] = y_position + 50 # Add some padding
197
 
198
- # Make the body itself the first element, and return its children
199
- elements = body_element['children']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  return elements
202
 
203
- def process_element(element, x_position, y_position, max_width, depth=0):
204
- """Process a single HTML element"""
205
  if depth > 10: # Limit recursion depth
206
  return None
207
 
208
- tag_name = element.name.lower()
209
- element_height = 50 # Default height for elements
210
-
211
- # Skip script, style tags
212
- if tag_name in ['script', 'style', 'meta', 'link']:
213
  return None
214
 
215
- # Create element data dictionary
 
 
 
 
 
 
 
216
  element_data = {
 
217
  'tagName': tag_name,
218
  'x': x_position,
219
  'y': y_position,
220
- 'width': max_width,
221
- 'height': element_height,
 
222
  }
223
 
224
- # Get element classes
225
- element_classes = element.get('class', [])
226
- if element_classes:
227
- if isinstance(element_classes, list):
228
- element_data['className'] = ' '.join(element_classes)
229
- else:
230
- element_data['className'] = element_classes
231
 
232
- # Get element ID
233
- element_id = element.get('id')
234
- if element_id:
235
- element_data['id'] = element_id
 
236
 
237
- # Handle different types of elements
238
- if tag_name in ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a']:
239
- element_data['type'] = 'text'
240
  text_content = element.get_text().strip()
241
  element_data['content'] = text_content
242
- element_height = 20 # Default height for text
243
 
244
- # Adjust height based on text length
245
- if text_content:
246
- num_lines = len(text_content) // 50 + 1 # Rough estimate
247
- element_height = max(20, num_lines * 20) # Min 20px, 20px per line
248
-
249
- # Basic style properties
250
- element_data['style'] = {
251
- 'color': '#000000',
252
- 'fontSize': '16px',
253
- 'fontWeight': 'normal',
254
- 'lineHeight': '1.5'
255
- }
256
 
257
- # Adjust style based on tag
258
- if tag_name.startswith('h'):
259
- size = 26 - int(tag_name[1]) # h1 = 25px, h2 = 24px, etc.
260
- element_data['style']['fontSize'] = f"{size}px"
261
- element_data['style']['fontWeight'] = 'bold'
262
- element_height = size * 1.5 # Adjust height based on font size
263
 
264
- elif tag_name == 'img':
265
- element_data['type'] = 'image'
266
  element_data['src'] = element.get('src', '')
267
  element_data['alt'] = element.get('alt', '')
268
- element_height = 200 # Default height for images
269
-
270
- elif tag_name in ['div', 'section', 'article', 'header', 'footer', 'main', 'nav']:
271
- # Container elements
272
- element_data['type'] = 'div'
273
- element_data['style'] = {
274
- 'backgroundColor': 'transparent',
275
- 'borderRadius': '0px'
276
- }
 
 
 
 
 
 
 
277
 
278
  # Process children
279
  children = []
280
  child_y_position = 0
 
 
 
 
 
 
 
281
 
282
- for child in element.find_all(['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'span', 'a', 'section', 'article', 'nav'], recursive=False):
283
- child_data = process_element(child, 5, child_y_position, max_width - 10, depth + 1)
 
284
  if child_data:
285
  children.append(child_data)
286
- child_y_position += child_data['height'] + 5 # Add spacing
 
 
 
 
 
 
 
 
287
 
288
  if children:
289
  element_data['children'] = children
290
- element_data['type'] = 'container'
291
- element_height = child_y_position + 10 # Total height of children + padding
292
- else:
293
- # Check if there's direct text content
294
- text_content = element.get_text(strip=True)
295
- if text_content:
296
- element_data['type'] = 'text'
297
- element_data['content'] = text_content
298
- element_data['style'] = {
299
- 'color': '#000000',
300
- 'fontSize': '16px'
301
- }
302
- element_height = 40 # Default height for text containers
303
-
304
- # Update height
305
- element_data['height'] = element_height
306
 
307
  return element_data
308
 
309
- def extract_styles_bs(soup, base_url):
310
- """Extract CSS styles from the webpage using BeautifulSoup"""
311
- styles = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- # Extract inline styles
314
- for element in soup.find_all(style=True):
315
- classes = element.get('class', [])
316
- if classes:
317
- class_str = '.'.join(classes)
318
- styles[f".{class_str}"] = element['style']
319
 
320
- # Extract style tags
321
- for style_tag in soup.find_all('style'):
322
- css_text = style_tag.string
323
- if css_text:
324
- parsed_styles = parse_css(css_text)
325
- styles.update(parsed_styles)
 
 
 
 
326
 
327
- # Extract linked stylesheets
328
- for link in soup.find_all('link', rel='stylesheet'):
329
- href = link.get('href')
330
- if href:
331
- # Make absolute URL if relative
332
- if not href.startswith(('http://', 'https://')):
333
- href = urllib.parse.urljoin(base_url, href)
334
-
335
- try:
336
- css_response = requests.get(href, timeout=5)
337
- if css_response.ok:
338
- parsed_styles = parse_css(css_response.text)
339
- styles.update(parsed_styles)
340
- except Exception as e:
341
- logger.warning(f"Failed to fetch stylesheet {href}: {e}")
342
 
343
- return styles
 
 
 
 
 
 
344
 
345
- def parse_css(css_text):
346
- """Parse CSS text into a dictionary of selectors and styles"""
347
- styles = {}
348
- try:
349
- sheet = cssutils.parseString(css_text)
350
- for rule in sheet:
351
- if rule.type == rule.STYLE_RULE:
352
- selector = rule.selectorText
353
- style_dict = {}
354
- for property in rule.style:
355
- style_dict[property.name] = property.value
356
- styles[selector] = style_dict
357
- except Exception as e:
358
- logger.warning(f"CSS parsing error: {e}")
359
 
360
- return styles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- def apply_styles_to_elements(elements, styles):
363
- """Apply CSS styles to elements"""
364
- for element in elements:
365
- if 'className' in element and element['className']:
366
- class_names = element['className'].split()
367
- for class_name in class_names:
368
- selector = '.' + class_name
369
- if selector in styles:
370
- apply_style(element, styles[selector])
 
 
371
 
372
- if 'id' in element and element['id']:
373
- selector = '#' + element['id']
374
- if selector in styles:
375
- apply_style(element, styles[selector])
376
-
377
- # Apply styles to children recursively
378
- if 'children' in element and element['children']:
379
- apply_styles_to_elements(element['children'], styles)
380
 
381
- def apply_style(element, style_dict):
382
- """Apply a CSS style to an element"""
383
- if 'style' not in element:
384
- element['style'] = {}
385
-
386
- # Apply properties to element style
387
- if isinstance(style_dict, dict):
388
- for key, value in style_dict.items():
389
- element['style'][key] = value
390
- elif isinstance(style_dict, str):
391
- # Parse inline style string
392
- for item in style_dict.split(';'):
393
- if ':' in item:
394
- key, value = item.split(':', 1)
395
- key = key.strip()
396
- value = value.strip()
397
- if key and value:
398
- element['style'][key] = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
  if __name__ == "__main__":
401
  port = int(os.environ.get("PORT", 7860))
402
- app.run(host="0.0.0.0", port=port)
 
17
  # Setup logging
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
+ cssutils.log.setLevel(logging.CRITICAL) # Suppress CSS parsing warnings
21
 
22
  @app.route('/')
23
  def home():
24
+ # ... keep existing code (HTML for homepage)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  @app.route('/api/convert', methods=['POST'])
27
  def convert_website():
 
51
  'Accept-Language': 'en-US,en;q=0.9',
52
  }
53
 
54
+ response = requests.get(url, headers=headers, timeout=20)
55
  response.raise_for_status() # Raise an exception for HTTP errors
56
 
57
+ html_content = response.text
 
58
 
59
+ # Parse the HTML content
60
+ soup = BeautifulSoup(html_content, 'html.parser')
61
 
62
+ # Extract all CSS styles (improved method)
63
+ all_styles = extract_all_css(soup, url)
64
 
65
+ # Extract the page elements using BeautifulSoup
66
+ elements = extract_elements_improved(soup, all_styles)
67
 
68
+ # Estimate page height based on content
69
  estimated_height = viewport_height
70
  if elements:
71
  # Find the maximum y-coordinate plus height
 
95
  logger.error(traceback.format_exc())
96
  return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500
97
 
98
+ def extract_all_css(soup, base_url):
99
+ """Extract all CSS from the page: inline, style tags, and external stylesheets"""
100
+ all_styles = {}
101
+
102
+ # 1. Extract inline styles
103
+ for element in soup.find_all(style=True):
104
+ element_id = element.get('id')
105
+ element_classes = element.get('class', [])
106
+
107
+ # Create selectors for this element
108
+ selectors = []
109
+ if element_id:
110
+ selectors.append(f"#{element_id}")
111
+ if element_classes:
112
+ for cls in element_classes:
113
+ selectors.append(f".{cls}")
114
+ if not selectors: # Fallback to tag name
115
+ selectors.append(element.name)
116
+
117
+ # Store inline style for each selector
118
+ inline_style = parse_inline_style(element['style'])
119
+ for selector in selectors:
120
+ all_styles[selector] = inline_style
121
 
122
+ # 2. Extract style tags
123
+ for style_tag in soup.find_all('style'):
124
+ if style_tag.string:
125
+ css_dict = parse_css(style_tag.string)
126
+ all_styles.update(css_dict)
127
+
128
+ # 3. Extract linked stylesheets
129
+ for link in soup.find_all('link', rel='stylesheet'):
130
+ href = link.get('href')
131
+ if not href:
132
+ continue
133
+
134
+ # Make absolute URL if relative
135
+ if not href.startswith(('http://', 'https://')):
136
+ href = urllib.parse.urljoin(base_url, href)
137
+
138
+ try:
139
+ css_response = requests.get(href, timeout=10)
140
+ if css_response.ok:
141
+ css_dict = parse_css(css_response.text)
142
+ all_styles.update(css_dict)
143
+ except Exception as e:
144
+ logger.warning(f"Failed to fetch stylesheet {href}: {e}")
145
+
146
+ # 4. Add computed styles for common elements
147
+ add_default_styles(all_styles)
148
+
149
+ return all_styles
150
+
151
+ def parse_inline_style(style_text):
152
+ """Parse inline style string into a dictionary"""
153
+ style_dict = {}
154
+ if not style_text:
155
+ return style_dict
156
+
157
+ # Split style string into individual properties
158
+ for item in style_text.split(';'):
159
+ if ':' in item:
160
+ prop, value = item.split(':', 1)
161
+ prop = prop.strip().lower()
162
+ value = value.strip()
163
+ if prop and value:
164
+ style_dict[prop] = value
165
+
166
+ return style_dict
167
+
168
+ def parse_css(css_text):
169
+ """Parse CSS text into a dictionary of selectors and styles"""
170
+ styles = {}
171
+
172
+ try:
173
+ sheet = cssutils.parseString(css_text)
174
+ for rule in sheet:
175
+ # Only handle style rules (not @media, etc.)
176
+ if rule.type == rule.STYLE_RULE:
177
+ selector = rule.selectorText
178
+ style_dict = {}
179
+
180
+ for prop in rule.style:
181
+ if prop.name and prop.value:
182
+ style_dict[prop.name.lower()] = prop.value
183
+
184
+ # Add to styles, merging if selector already exists
185
+ if selector in styles:
186
+ styles[selector].update(style_dict)
187
+ else:
188
+ styles[selector] = style_dict
189
+ except Exception as e:
190
+ logger.warning(f"CSS parsing error: {e}")
191
+
192
+ return styles
193
+
194
+ def add_default_styles(styles):
195
+ """Add default styles for common HTML elements"""
196
+ # Body defaults
197
+ styles.setdefault('body', {}).update({
198
+ 'margin': '0px',
199
+ 'font-family': 'Arial, sans-serif',
200
+ 'color': '#000000',
201
+ 'font-size': '16px'
202
+ })
203
+
204
+ # Heading defaults
205
+ styles.setdefault('h1', {}).update({'font-size': '32px', 'font-weight': 'bold', 'margin': '21.44px 0'})
206
+ styles.setdefault('h2', {}).update({'font-size': '24px', 'font-weight': 'bold', 'margin': '19.92px 0'})
207
+ styles.setdefault('h3', {}).update({'font-size': '18px', 'font-weight': 'bold', 'margin': '18.72px 0'})
208
+ styles.setdefault('h4', {}).update({'font-size': '16px', 'font-weight': 'bold', 'margin': '21.28px 0'})
209
 
210
+ # Link defaults
211
+ styles.setdefault('a', {}).update({'color': '#0000EE', 'text-decoration': 'underline'})
212
+
213
+ # Button defaults
214
+ styles.setdefault('button', {}).update({
215
+ 'background-color': '#F0F0F0',
216
+ 'border': '1px solid #CCCCCC',
217
+ 'padding': '4px 8px',
218
+ 'border-radius': '2px'
219
+ })
220
+
221
+ # Input defaults
222
+ styles.setdefault('input', {}).update({
223
+ 'border': '1px solid #CCCCCC',
224
+ 'padding': '2px 4px'
225
+ })
226
+
227
+ def extract_elements_improved(soup, styles):
228
+ """Extract elements from the webpage with improved CSS handling"""
229
+ elements = []
230
+
231
+ # Get the body element
232
  body = soup.find('body')
233
  if not body:
234
  return elements
235
 
236
+ # Start position for elements
237
+ x_offset = 0
238
+ y_position = 0
239
+ viewport_width = 1440 # Default width
 
 
 
 
 
 
240
 
241
+ # Create a mapping of elements to their computed styles
242
+ element_styles = {}
 
 
 
 
243
 
244
+ # Process main content blocks first
245
+ main_blocks = body.find_all(['div', 'header', 'main', 'nav', 'footer', 'section'], recursive=False)
246
 
247
+ if not main_blocks: # If no main blocks, use all direct children
248
+ main_blocks = body.find_all(recursive=False)
249
+
250
+ # Process each main block
251
+ for block in main_blocks:
252
+ block_data = process_element_with_styles(block, x_offset, y_position, viewport_width, styles)
253
+ if block_data:
254
+ elements.append(block_data)
255
+ y_position += block_data['height'] + 10 # Add spacing between blocks
256
+
257
+ # If no elements were found, try to extract text directly
258
+ if not elements and body.text.strip():
259
+ text_element = {
260
+ 'type': 'text',
261
+ 'tagName': 'p',
262
+ 'x': 0,
263
+ 'y': 0,
264
+ 'width': viewport_width,
265
+ 'height': 100,
266
+ 'content': body.text.strip(),
267
+ 'style': {
268
+ 'color': '#000000',
269
+ 'fontSize': '16px',
270
+ 'fontFamily': 'Arial, sans-serif'
271
+ }
272
+ }
273
+ elements.append(text_element)
274
 
275
  return elements
276
 
277
+ def process_element_with_styles(element, x_position, y_position, parent_width, styles, depth=0):
278
+ """Process a single HTML element with its styles"""
279
  if depth > 10: # Limit recursion depth
280
  return None
281
 
282
+ tag_name = element.name.lower() if hasattr(element, 'name') else None
283
+ if not tag_name or tag_name in ['script', 'style', 'meta', 'link', 'noscript']:
 
 
 
284
  return None
285
 
286
+ # Get element's classes and ID
287
+ elem_classes = element.get('class', [])
288
+ elem_id = element.get('id')
289
+
290
+ # Calculate element's computed style
291
+ computed_style = compute_element_style(element, tag_name, elem_id, elem_classes, styles)
292
+
293
+ # Create base element data
294
  element_data = {
295
+ 'type': get_element_type(tag_name),
296
  'tagName': tag_name,
297
  'x': x_position,
298
  'y': y_position,
299
+ 'width': calc_element_width(computed_style, parent_width),
300
+ 'height': 50, # Default height, will be adjusted later
301
+ 'style': {}
302
  }
303
 
304
+ # Set element ID and class if present
305
+ if elem_id:
306
+ element_data['id'] = elem_id
 
 
 
 
307
 
308
+ if elem_classes:
309
+ if isinstance(elem_classes, list):
310
+ element_data['className'] = ' '.join(elem_classes)
311
+ else:
312
+ element_data['className'] = elem_classes
313
 
314
+ # Process specific element types
315
+ if element_data['type'] == 'text':
 
316
  text_content = element.get_text().strip()
317
  element_data['content'] = text_content
 
318
 
319
+ # Set text styles
320
+ extract_text_styles(element_data, computed_style)
 
 
 
 
 
 
 
 
 
 
321
 
322
+ # Calculate height based on text content
323
+ element_data['height'] = calc_text_height(text_content, computed_style)
 
 
 
 
324
 
325
+ elif element_data['type'] == 'image':
326
+ # Set image source if available
327
  element_data['src'] = element.get('src', '')
328
  element_data['alt'] = element.get('alt', '')
329
+
330
+ # Set height for images
331
+ if 'height' in computed_style:
332
+ try:
333
+ element_data['height'] = parse_dimension(computed_style['height'], parent_width)
334
+ except:
335
+ element_data['height'] = 200 # Default height
336
+ else:
337
+ element_data['height'] = 200
338
+
339
+ # Extract background styles
340
+ extract_background_styles(element_data, computed_style)
341
+
342
+ elif element_data['type'] in ['div', 'container', 'rectangle']:
343
+ # Process container elements
344
+ extract_container_styles(element_data, computed_style)
345
 
346
  # Process children
347
  children = []
348
  child_y_position = 0
349
+ child_x_position = 0
350
+
351
+ # Apply padding if present
352
+ padding_left = parse_dimension(computed_style.get('padding-left', '0'), parent_width)
353
+ child_x_position += padding_left
354
+
355
+ available_width = element_data['width'] - (padding_left + parse_dimension(computed_style.get('padding-right', '0'), parent_width))
356
 
357
+ # Process child elements
358
+ for child in element.find_all(['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'span', 'a', 'button', 'input', 'form'], recursive=False):
359
+ child_data = process_element_with_styles(child, child_x_position, child_y_position, available_width, styles, depth + 1)
360
  if child_data:
361
  children.append(child_data)
362
+ if 'display' in computed_style and computed_style['display'] == 'flex':
363
+ # Handle flex layout (simplified)
364
+ if computed_style.get('flex-direction') == 'row':
365
+ child_x_position += child_data['width'] + 5
366
+ else:
367
+ child_y_position += child_data['height'] + 5
368
+ else:
369
+ # Default block layout
370
+ child_y_position += child_data['height'] + 5
371
 
372
  if children:
373
  element_data['children'] = children
374
+
375
+ # Adjust container height based on children
376
+ if children and 'display' not in computed_style or computed_style.get('display') != 'flex':
377
+ last_child = children[-1]
378
+ element_data['height'] = last_child['y'] - element_data['y'] + last_child['height'] + 10
379
+
380
+ # Apply common styles (border, margin, etc)
381
+ apply_common_styles(element_data, computed_style)
382
+
383
+ # If height is unreasonably small, set a minimum
384
+ if element_data['height'] < 10:
385
+ element_data['height'] = 10
 
 
 
 
386
 
387
  return element_data
388
 
389
+ def get_element_type(tag_name):
390
+ """Determine element type based on tag name"""
391
+ if tag_name in ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'label']:
392
+ return 'text'
393
+ elif tag_name == 'img':
394
+ return 'image'
395
+ elif tag_name in ['div', 'section', 'article', 'header', 'footer', 'main', 'form']:
396
+ return 'div'
397
+ elif tag_name == 'button':
398
+ return 'rectangle' # Represent as a rectangle with text
399
+ elif tag_name == 'input':
400
+ return 'rectangle' # Represent as a rectangle
401
+ else:
402
+ return 'div' # Default type
403
+
404
+ def compute_element_style(element, tag_name, elem_id, elem_classes, styles):
405
+ """Compute the final style for an element by cascading CSS rules"""
406
+ computed_style = {}
407
 
408
+ # 1. Apply tag-level styles
409
+ if tag_name in styles:
410
+ computed_style.update(styles[tag_name])
 
 
 
411
 
412
+ # 2. Apply class styles
413
+ if isinstance(elem_classes, list):
414
+ for cls in elem_classes:
415
+ class_selector = f".{cls}"
416
+ if class_selector in styles:
417
+ computed_style.update(styles[class_selector])
418
+ elif elem_classes:
419
+ class_selector = f".{elem_classes}"
420
+ if class_selector in styles:
421
+ computed_style.update(styles[class_selector])
422
 
423
+ # 3. Apply ID styles (highest specificity)
424
+ if elem_id and f"#{elem_id}" in styles:
425
+ computed_style.update(styles[f"#{elem_id}"])
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
+ # 4. Apply inline styles (overrides everything)
428
+ inline_style = element.get('style')
429
+ if inline_style:
430
+ parsed_inline = parse_inline_style(inline_style)
431
+ computed_style.update(parsed_inline)
432
+
433
+ return computed_style
434
 
435
+ def parse_dimension(value, container_size):
436
+ """Parse dimension values (px, %, em, etc)"""
437
+ if not value or not isinstance(value, str):
438
+ return 0
 
 
 
 
 
 
 
 
 
 
439
 
440
+ value = value.strip().lower()
441
+
442
+ # Handle pixel values
443
+ if value.endswith('px'):
444
+ try:
445
+ return float(value[:-2])
446
+ except:
447
+ return 0
448
+
449
+ # Handle percentage values
450
+ elif value.endswith('%'):
451
+ try:
452
+ percentage = float(value[:-1]) / 100
453
+ return container_size * percentage
454
+ except:
455
+ return 0
456
+
457
+ # Handle em values (approximate)
458
+ elif value.endswith('em'):
459
+ try:
460
+ em_value = float(value[:-2])
461
+ return em_value * 16 # Assuming 1em = 16px
462
+ except:
463
+ return 0
464
+
465
+ # Handle rem values (approximate)
466
+ elif value.endswith('rem'):
467
+ try:
468
+ rem_value = float(value[:-3])
469
+ return rem_value * 16 # Assuming 1rem = 16px
470
+ except:
471
+ return 0
472
+
473
+ # Handle vh/vw values (viewport height/width)
474
+ elif value.endswith('vh'):
475
+ try:
476
+ vh_value = float(value[:-2]) / 100
477
+ return vh_value * 900 # Assuming viewport height is 900px
478
+ except:
479
+ return 0
480
+ elif value.endswith('vw'):
481
+ try:
482
+ vw_value = float(value[:-2]) / 100
483
+ return vw_value * 1440 # Assuming viewport width is 1440px
484
+ except:
485
+ return 0
486
+
487
+ # Handle numeric values
488
+ elif value.isdigit():
489
+ return float(value)
490
+
491
+ # Handle auto (use container size)
492
+ elif value == 'auto':
493
+ return container_size
494
+
495
+ # Default fallback
496
+ return 0
497
 
498
+ def calc_element_width(style, parent_width):
499
+ """Calculate element width based on its style"""
500
+ # Check if width is explicitly set
501
+ if 'width' in style:
502
+ width_value = style['width']
503
+ return parse_dimension(width_value, parent_width)
504
+
505
+ # Check for max-width
506
+ if 'max-width' in style:
507
+ max_width = parse_dimension(style['max-width'], parent_width)
508
+ return min(parent_width, max_width)
509
 
510
+ # Default: use parent width
511
+ return parent_width
 
 
 
 
 
 
512
 
513
+ def calc_text_height(text, style):
514
+ """Calculate text height based on content and style"""
515
+ if not text:
516
+ return 20
517
+
518
+ # Get font size
519
+ font_size = 16 # Default
520
+ if 'font-size' in style:
521
+ font_size_value = style['font-size']
522
+ if isinstance(font_size_value, str):
523
+ if font_size_value.endswith('px'):
524
+ try:
525
+ font_size = float(font_size_value[:-2])
526
+ except:
527
+ pass
528
+ elif font_size_value.endswith('em'):
529
+ try:
530
+ font_size = float(font_size_value[:-2]) * 16
531
+ except:
532
+ pass
533
+
534
+ # Get line height
535
+ line_height = 1.2 # Default
536
+ if 'line-height' in style:
537
+ line_height_value = style['line-height']
538
+ if isinstance(line_height_value, str):
539
+ if line_height_value.endswith('px'):
540
+ try:
541
+ line_height = float(line_height_value[:-2]) / font_size
542
+ except:
543
+ pass
544
+ else:
545
+ try:
546
+ line_height = float(line_height_value)
547
+ except:
548
+ pass
549
+
550
+ # Estimate number of lines needed
551
+ text_length = len(text)
552
+ chars_per_line = 70 # Rough estimate
553
+ num_lines = max(1, (text_length / chars_per_line))
554
+
555
+ # Calculate height
556
+ return max(20, int(font_size * line_height * num_lines))
557
+
558
+ def extract_text_styles(element_data, style):
559
+ """Extract text-related styles from computed style"""
560
+ # Text color
561
+ if 'color' in style:
562
+ element_data['style']['color'] = style['color']
563
+ else:
564
+ element_data['style']['color'] = '#000000' # Default black
565
+
566
+ # Font size
567
+ if 'font-size' in style:
568
+ element_data['style']['fontSize'] = style['font-size']
569
+ else:
570
+ tag_name = element_data.get('tagName', '')
571
+ if tag_name.startswith('h'):
572
+ # Default heading sizes
573
+ heading_level = int(tag_name[1])
574
+ size = 32 - ((heading_level - 1) * 4)
575
+ element_data['style']['fontSize'] = f"{size}px"
576
+ else:
577
+ element_data['style']['fontSize'] = '16px' # Default
578
+
579
+ # Font weight
580
+ if 'font-weight' in style:
581
+ element_data['style']['fontWeight'] = style['font-weight']
582
+ else:
583
+ tag_name = element_data.get('tagName', '')
584
+ if tag_name.startswith('h'):
585
+ element_data['style']['fontWeight'] = 'bold'
586
+ else:
587
+ element_data['style']['fontWeight'] = 'normal'
588
+
589
+ # Font family
590
+ if 'font-family' in style:
591
+ element_data['style']['fontFamily'] = style['font-family']
592
+
593
+ # Text alignment
594
+ if 'text-align' in style:
595
+ element_data['style']['textAlign'] = style['text-align']
596
+
597
+ # Text decoration
598
+ if 'text-decoration' in style:
599
+ element_data['style']['textDecoration'] = style['text-decoration']
600
+
601
+ def extract_container_styles(element_data, style):
602
+ """Extract container-related styles from computed style"""
603
+ # Background color
604
+ if 'background-color' in style:
605
+ element_data['style']['backgroundColor'] = style['background-color']
606
+
607
+ # Display type
608
+ if 'display' in style:
609
+ element_data['style']['display'] = style['display']
610
+
611
+ # Flex-related properties
612
+ if 'display' in style and style['display'] == 'flex':
613
+ if 'flex-direction' in style:
614
+ element_data['style']['flexDirection'] = style['flex-direction']
615
+ if 'justify-content' in style:
616
+ element_data['style']['justifyContent'] = style['justify-content']
617
+ if 'align-items' in style:
618
+ element_data['style']['alignItems'] = style['align-items']
619
+
620
+ def extract_background_styles(element_data, style):
621
+ """Extract background-related styles from computed style"""
622
+ if 'background-color' in style:
623
+ element_data['style']['backgroundColor'] = style['background-color']
624
+
625
+ if 'background-image' in style:
626
+ bg_image = style['background-image']
627
+ if bg_image.startswith('url(') and bg_image.endswith(')'):
628
+ image_url = bg_image[4:-1].strip('"\'')
629
+ element_data['style']['backgroundImage'] = image_url
630
+
631
+ def apply_common_styles(element_data, style):
632
+ """Apply common styles that apply to all elements"""
633
+ # Border properties
634
+ if 'border' in style:
635
+ element_data['style']['border'] = style['border']
636
+ else:
637
+ # Individual border properties
638
+ for side in ['top', 'right', 'bottom', 'left']:
639
+ border_key = f'border-{side}'
640
+ if border_key in style:
641
+ element_data['style'][border_key] = style[border_key]
642
+
643
+ if 'border-radius' in style:
644
+ element_data['style']['borderRadius'] = style['border-radius']
645
+
646
+ # Opacity
647
+ if 'opacity' in style:
648
+ element_data['style']['opacity'] = style['opacity']
649
+
650
+ # Visibility
651
+ if 'visibility' in style:
652
+ element_data['style']['visibility'] = style['visibility']
653
+
654
+ # Box shadow
655
+ if 'box-shadow' in style:
656
+ element_data['style']['boxShadow'] = style['box-shadow']
657
 
658
  if __name__ == "__main__":
659
  port = int(os.environ.get("PORT", 7860))
660
+ app.run(host="0.0.0.0", port=port)