webcode-tk 1.2.7__tar.gz → 1.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webcode-tk
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: A set of tools for inspecting HTML and CSS code
5
5
  License: MIT
6
6
  License-File: LICENSE.rst
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "webcode-tk"
3
- version = "1.2.7"
3
+ version = "1.2.9"
4
4
  description = "A set of tools for inspecting HTML and CSS code"
5
5
  authors = ["hundredvisionsguy <cwinikka@gmail.com>"]
6
6
  keywords = ["html", "validation", "css", ]
@@ -28,7 +28,7 @@ lxml = "^6.0.0"
28
28
  mechanicalsoup = "^1.4.0"
29
29
  pytest-mock = "^3.15.1"
30
30
 
31
- [tool.poetry.dev-dependencies]
31
+ [tool.poetry.group.dev.dependencies]
32
32
  pytest = "*"
33
33
  mkdocs = "*"
34
34
  mkgendocs = "*"
@@ -40,4 +40,4 @@
40
40
  - **`validator_tools`**: sends HTML or CSS code to the W3C Validator to check
41
41
  for errors.
42
42
  """
43
- __version__ = "1.2.7"
43
+ __version__ = "1.2.9"
@@ -130,6 +130,7 @@ def generate_contrast_report(
130
130
 
131
131
  # Check if fails WCAG level
132
132
  result = item.get(wcag_level_key)
133
+
133
134
  if result:
134
135
  result = result.lower()
135
136
  else:
@@ -154,37 +155,42 @@ def generate_contrast_report(
154
155
  actual_ratio = item.get("contrast_ratio")
155
156
  actual_ratio = round(actual_ratio, 1)
156
157
 
157
- # append fail message to project_files
158
- msg = f"{result}: in {current_file}, the {element} failed WCAG "
159
- msg += f"{wcag_level} contrast requirements (ratio: "
160
- msg += f"{actual_ratio}, required: {target_ratio})."
158
+ msg = f"{result}: in {current_file}, the <{element}> element"
159
+ msg += f" WCAG {wcag_level} contrast requirements with a "
160
+ msg += f"ratio of {actual_ratio} (min: {target_ratio})."
161
+
161
162
  if msg not in project_files[current_file]:
162
163
  project_files[current_file].append(msg)
163
164
 
164
165
  # Check for html docs with no fails.
165
166
  for file in project_files:
166
167
  errors = project_files.get(file)
168
+
169
+ # Check for global selectors (html, *, or body) and calculate contrast
170
+ global_color_result = css_tools.get_global_color_report(project_path)
171
+
172
+ if isinstance(global_color_result, list):
173
+ for result in global_color_result:
174
+ if file in result:
175
+ # Always add global color failures
176
+ if "fail" in result:
177
+ error_msg = result.split(file)[1]
178
+ msg = f"fail: in {file}, {error_msg.strip()}"
179
+ # Add to errors list if not already there
180
+ if msg not in project_files[file]:
181
+ project_files[file].append(msg)
182
+
183
+ # Now append to report
167
184
  if errors:
168
185
  if "has no CSS" in errors[0]:
169
186
  report.append(errors[0])
170
187
  else:
171
188
  report.extend(errors)
172
189
  else:
173
- # Check for global selectors (html, *, or body) and calculate
174
- # contrast
175
- global_color_result = css_tools.get_global_color_report(
176
- project_path
177
- )
178
- if isinstance(global_color_result, list):
179
- for result in global_color_result:
180
- if "fail" in result:
181
- if file in result:
182
- error_msg = result.split(file)[1]
183
- msg = f"fail: in {file}, {error_msg.strip()}"
184
- else:
185
- msg = f"pass: {file} passes color contrast."
186
-
190
+ # File has no element-level failures
191
+ msg = f"pass: {file} passes color contrast."
187
192
  report.append(msg)
193
+
188
194
  return report
189
195
 
190
196
 
@@ -553,8 +559,18 @@ def apply_browser_defaults(
553
559
  element_styles = {}
554
560
  default_specificity = css_tools.get_specificity("")
555
561
 
556
- for element in soup.find_all():
557
- if not element.is_empty_element and element.get_text(strip=True):
562
+ for element in soup.descendants:
563
+ # Skip non-tag elements
564
+ if not isinstance(element, Tag):
565
+ continue
566
+
567
+ # Always include html and body elements for inheritance chain
568
+ is_structural = element.name in ["html", "body"]
569
+ has_text = not element.is_empty_element and element.get_text(
570
+ strip=True
571
+ )
572
+
573
+ if is_structural or has_text:
558
574
  # The element object itself becomes the key
559
575
  element_styles[element] = {
560
576
  "color": {
@@ -583,6 +599,8 @@ def apply_browser_defaults(
583
599
  },
584
600
  }
585
601
 
602
+ if element.name in ["html", "body"]:
603
+ print(f" ✓ Added <{element.name}> to element_styles")
586
604
  # Apply element-specific defaults
587
605
  # Apply link colors
588
606
  if element.name == "a":
@@ -689,8 +707,12 @@ def compute_element_styles(
689
707
  dict: Dictionary mapping elements to their final computed styles
690
708
  after applying CSS cascade and inheritance.
691
709
  """
692
- # Step 1: Start with default styles
693
- computed_styles = copy.deepcopy(default_styles)
710
+ # ✅ NEW: Shallow copy of dict, deep copy only the style values
711
+ computed_styles = {}
712
+ for element, styles in default_styles.items():
713
+ # Keep the SAME element object (preserves parent relationship)
714
+ # but deep copy its styles dictionary
715
+ computed_styles[element] = copy.deepcopy(styles)
694
716
 
695
717
  # Step 2: Apply all CSS rules (cascade resolution)
696
718
  for rules_list in css_rules:
@@ -1122,145 +1144,144 @@ def apply_inheritance(soup: BeautifulSoup, computed_styles: dict) -> None:
1122
1144
  def apply_css_inheritance(computed_styles: dict) -> None:
1123
1145
  """
1124
1146
  Apply traditional CSS inheritance for inheritable properties.
1125
-
1126
- Args:
1127
- computed_styles (dict): Dictionary mapping elements to their
1128
- computed styles (modified in place).
1129
-
1130
- Returns:
1131
- None: Modifies computed_styles dictionary in place.
1147
+ Only inherit from the closest ancestor with an explicit rule.
1132
1148
  """
1133
1149
  inheritable_props = {"color", "font-size", "font-weight"}
1134
1150
 
1135
- for parent_element in computed_styles:
1136
- parent_styles = computed_styles[parent_element]
1151
+ for child_element in computed_styles:
1152
+ child_styles = computed_styles[child_element]
1137
1153
 
1138
- # Find all children that also have text content (in computed_styles)
1139
- children = parent_element.find_all()
1140
- children_with_styles = [
1141
- child for child in children if child in computed_styles
1142
- ]
1154
+ # find closest ancestor with explicit value
1155
+ for prop in inheritable_props:
1156
+ child_prop = child_styles.get(prop)
1143
1157
 
1144
- for child_element in children_with_styles:
1145
- child_styles = computed_styles[child_element]
1146
-
1147
- # Apply inheritance for each inheritable property
1148
- for prop in inheritable_props:
1149
- parent_prop = parent_styles.get(prop)
1150
-
1151
- if parent_prop: # Parent has this property
1152
- child_prop = child_styles.get(prop)
1153
- should_inherit = False
1154
-
1155
- if child_prop is None:
1156
- # Child has no property at all - inherit
1157
- should_inherit = True
1158
- elif child_prop.get("source") == "default":
1159
- # Child has default style - inheritance wins
1160
- should_inherit = True
1161
- else:
1162
- # Child has explicit CSS rule - don't inherit
1163
- should_inherit = False
1164
-
1165
- if should_inherit:
1166
- child_styles[prop] = {
1167
- "value": parent_prop["value"],
1168
- "specificity": parent_prop["specificity"],
1169
- "source": "inheritance",
1170
- "selector": parent_prop.get(
1171
- "selector", "inherited"
1172
- ),
1173
- "css_file": parent_prop.get("css_file"),
1174
- "css_source_type": parent_prop.get(
1175
- "css_source_type"
1176
- ),
1177
- "inherited_from": parent_element,
1178
- }
1158
+ # Skip if child has explicit CSS rule (not default)
1159
+ if child_prop and child_prop.get("source") not in [
1160
+ "default",
1161
+ None,
1162
+ ]:
1163
+ continue
1164
+
1165
+ # Walk up tree to find first ancestor with explicit value
1166
+ current = child_element.parent
1167
+ inherited_value = None
1168
+
1169
+ while current and not inherited_value:
1170
+ if current in computed_styles:
1171
+ parent_styles = computed_styles[current]
1172
+ parent_prop = parent_styles.get(prop)
1173
+
1174
+ # Found a parent with this property
1175
+ if parent_prop:
1176
+ # Only inherit if parent has explicit rule
1177
+ if parent_prop.get("source") == "rule":
1178
+ inherited_value = parent_prop
1179
+ inherited_from = current
1180
+ break # ← Stop at first explicit ancestor
1181
+
1182
+ current = current.parent
1183
+
1184
+ # Apply inherited value if found
1185
+ if inherited_value:
1186
+ child_styles[prop] = {
1187
+ "value": inherited_value["value"],
1188
+ "specificity": inherited_value["specificity"],
1189
+ "source": "inheritance",
1190
+ "selector": inherited_value.get("selector", "inherited"),
1191
+ "css_file": inherited_value.get("css_file"),
1192
+ "css_source_type": inherited_value.get("css_source_type"),
1193
+ "inherited_from": inherited_from,
1194
+ }
1179
1195
 
1180
1196
 
1181
1197
  def apply_visual_background_inheritance(computed_styles: dict) -> None:
1182
1198
  """
1183
- Apply visual background colors to elements without explicit
1184
- backgrounds.
1199
+ Apply visual background colors to elements without explicit backgrounds.
1185
1200
 
1186
1201
  Args:
1187
1202
  computed_styles: a dictionary of elements with computed styles.
1188
1203
  Returns:
1189
1204
  None
1190
1205
  """
1191
- # Track which elements got new backgrounds this iteration
1192
- changes_made = True
1193
- max_iterations = 10 # Prevent infinite loops
1194
- iteration = 0
1195
-
1196
- while changes_made and iteration < max_iterations:
1197
- changes_made = False
1198
- iteration += 1
1199
-
1200
- for element in computed_styles:
1201
- element_styles = computed_styles[element]
1202
1206
 
1203
- # Skip if element already has background-color
1204
- if "background-color" in element_styles:
1205
- continue
1206
-
1207
- # Check for explicit backgrounds
1208
- if "background" in element_styles:
1209
- bg_value = element_styles["background"]["value"]
1207
+ for element in computed_styles:
1208
+ element_styles = computed_styles[element]
1210
1209
 
1211
- # Check if background contains a raster image
1212
- if contains_raster_image(bg_value):
1213
- # Mark as contrast-indeterminate - cannot analyze
1214
- element_styles["background-color"] = {
1215
- "value": None, # No usable color
1216
- "specificity": "000",
1217
- "contrast_analysis": "indeterminate",
1218
- "reason": "background_image_blocks_color_analysis",
1219
- "original_background": bg_value,
1220
- "visual_inheritance": False,
1221
- }
1222
- changes_made = True
1223
- continue
1224
- else:
1225
- # Has usable background shorthand - skip inheritance
1226
- continue
1210
+ # Check for BOTH background-color AND background
1211
+ current_bg = element_styles.get("background-color")
1212
+ has_background_shorthand = "background" in element_styles
1227
1213
 
1228
- # Element needs background inheritance
1229
- ancestor_bg = find_ancestor_background(element, computed_styles)
1214
+ # If element has background shorthand, process it for images
1215
+ if has_background_shorthand:
1216
+ bg_value = element_styles["background"]["value"]
1230
1217
 
1231
- if ancestor_bg.get("contrast_analysis") == "indeterminate":
1232
- # Ancestor is indeterminate - propagate that status
1218
+ # Check if background contains a raster image
1219
+ if contains_raster_image(bg_value):
1220
+ # Mark as contrast-indeterminate - cannot analyze
1233
1221
  element_styles["background-color"] = {
1234
1222
  "value": None,
1235
1223
  "specificity": "000",
1236
1224
  "contrast_analysis": "indeterminate",
1237
- "reason": ancestor_bg["reason"],
1238
- "inherited_from": ancestor_bg["source_element"],
1239
- "source": "visual_inheritance",
1240
- "original_background": ancestor_bg.get(
1241
- "original_background"
1242
- ),
1225
+ "reason": "background_image_blocks_color_analysis",
1226
+ "original_background": bg_value,
1227
+ "visual_inheritance": False,
1243
1228
  }
1244
- changes_made = True
1229
+ continue
1245
1230
  else:
1246
- # Normal inheritance - extract usable color
1247
- contrast_color = extract_contrast_color(ancestor_bg["value"])
1248
- effective_color = (
1249
- contrast_color if contrast_color else ancestor_bg["value"]
1250
- )
1251
- element_styles["background-color"] = {
1252
- "value": effective_color,
1253
- "specificity": "000",
1254
- "source": "visual_inheritance",
1255
- "inherited_from": ancestor_bg["source_element"],
1256
- "contrast_analysis": "determinable",
1257
- "original_background": (
1258
- ancestor_bg["value"]
1259
- if ancestor_bg["value"] != DEFAULT_GLOBAL_BACKGROUND
1260
- else None
1261
- ),
1262
- }
1263
- changes_made = True
1231
+ # Has usable background shorthand - skip inheritance
1232
+ continue
1233
+ if not current_bg:
1234
+ continue
1235
+
1236
+ bg_source = current_bg.get("source")
1237
+ bg_value = current_bg.get("value")
1238
+
1239
+ # Skip elements with explicit CSS rules
1240
+ if bg_source == "rule":
1241
+ continue
1242
+
1243
+ # Only update elements that still have the default white background
1244
+ # This prevents overwriting elements with existing proper backgrounds
1245
+ if bg_source != "default":
1246
+ continue
1247
+
1248
+ # Element needs background inheritance (lines 1120-1141)
1249
+ ancestor_bg = find_ancestor_background(element, computed_styles)
1250
+
1251
+ if not ancestor_bg or ancestor_bg.get("source") == "default":
1252
+ # No explicit background to inherit - keep browser default
1253
+ continue
1254
+
1255
+ if ancestor_bg.get("contrast_analysis") == "indeterminate":
1256
+ # Ancestor is indeterminate - propagate that status
1257
+ element_styles["background-color"] = {
1258
+ "value": None,
1259
+ "specificity": "000",
1260
+ "contrast_analysis": "indeterminate",
1261
+ "reason": ancestor_bg["reason"],
1262
+ "inherited_from": ancestor_bg["source_element"],
1263
+ "source": "visual_inheritance",
1264
+ "original_background": ancestor_bg.get("original_background"),
1265
+ }
1266
+
1267
+ else:
1268
+ # Normal inheritance - extract usable color
1269
+ contrast_color = extract_contrast_color(ancestor_bg["value"])
1270
+ effective_color = (
1271
+ contrast_color if contrast_color else ancestor_bg["value"]
1272
+ )
1273
+ element_styles["background-color"] = {
1274
+ "value": effective_color,
1275
+ "specificity": "000",
1276
+ "source": "visual_inheritance",
1277
+ "inherited_from": ancestor_bg["source_element"],
1278
+ "contrast_analysis": "determinable",
1279
+ "original_background": (
1280
+ ancestor_bg["value"]
1281
+ if ancestor_bg["value"] != DEFAULT_GLOBAL_BACKGROUND
1282
+ else None
1283
+ ),
1284
+ }
1264
1285
 
1265
1286
 
1266
1287
  def has_usable_background_color(background_prop: dict) -> bool:
@@ -1415,6 +1436,10 @@ def find_ancestor_background(element: Tag, computed_styles: dict) -> dict:
1415
1436
  # Accept both "rule" and "default" sources
1416
1437
  if bg_source in ["rule", "default"]:
1417
1438
  bg_value = current_styles[bg_prop]["value"]
1439
+ if bg_source == "default":
1440
+ # Skip ancestors with only default backgrounds
1441
+ skip_ancestor = True
1442
+ break
1418
1443
 
1419
1444
  # Does this ancestor already is indeterminate status
1420
1445
  if (
@@ -1531,6 +1556,26 @@ def analyze_elements_for_contrast(
1531
1556
  contrast_analysis = element_data["background-color"].get(
1532
1557
  "contrast_analysis"
1533
1558
  )
1559
+
1560
+ # Check if this element inherited both text and background colors
1561
+ text_color_source_type = extract_property_source(
1562
+ element_data["color"]
1563
+ ).get("source_type")
1564
+ bg_color_source_type = extract_property_source(
1565
+ element_data["background-color"]
1566
+ ).get("source_type")
1567
+
1568
+ # Determine if this is a global color failure
1569
+ inherited_text = text_color_source_type in [
1570
+ "inherited",
1571
+ "browser_default",
1572
+ ]
1573
+ inherited_bg = bg_color_source_type in [
1574
+ "visual_inheritance",
1575
+ "browser_default",
1576
+ ]
1577
+ is_global_failure = inherited_text and inherited_bg
1578
+
1534
1579
  if is_large:
1535
1580
  aaa_results = contrast_report.get("Large AAA")
1536
1581
  aa_results = contrast_report.get("Large AA")
@@ -1562,6 +1607,7 @@ def analyze_elements_for_contrast(
1562
1607
  "wcag_aa_pass": aa_results,
1563
1608
  "wcag_aaa_pass": aaa_results,
1564
1609
  "contrast_analysis": contrast_analysis,
1610
+ "global_color_failure": is_global_failure,
1565
1611
  }
1566
1612
 
1567
1613
  # add to results
@@ -1677,5 +1723,5 @@ if __name__ == "__main__":
1677
1723
  contrast_results = generate_contrast_report(project_path, "AAA")
1678
1724
  project_path = "tests/test_files/contrast_tool_test/"
1679
1725
  global_colors = css_tools.get_project_global_colors(project_path)
1680
- print(global_colors)
1681
- print(contrast_results)
1726
+ print()
1727
+ print()
@@ -2488,8 +2488,6 @@ def get_color_data(file: str, color_details: dict, level="aaa") -> tuple:
2488
2488
  Returns:
2489
2489
  color_data: a tuple with filename and results as a string"""
2490
2490
  selector = color_details.get("selector")
2491
- bg_color = color_details.get("background-color")
2492
- color = color_details.get("color")
2493
2491
  contrast_ratio = color_details.get("contrast_ratio")
2494
2492
  if level == "aaa":
2495
2493
  passes = color_details.get("passes_normal_aaa")
@@ -2498,8 +2496,8 @@ def get_color_data(file: str, color_details: dict, level="aaa") -> tuple:
2498
2496
  if passes:
2499
2497
  results = "passes global colors"
2500
2498
  else:
2501
- results = f"{selector} {bg_color} and {color} fail with a contrast"
2502
- results += f" ratio of {contrast_ratio}."
2499
+ results = f"the <{selector}> element fails WCAG {level.upper()}"
2500
+ results += f"contrast with a ratio of {contrast_ratio}."
2503
2501
  color_data = (file, results)
2504
2502
  return color_data
2505
2503
 
@@ -2677,6 +2675,7 @@ def get_properties_applied_report(project_dir: str, goals: dict) -> list:
2677
2675
  )
2678
2676
  if selector_type == "grouped_selector":
2679
2677
  sel_split = selector.split(",")
2678
+ sel_split = [s.strip() for s in sel_split]
2680
2679
  if element in sel_split:
2681
2680
  take_targetted_properties(
2682
2681
  properties_found,
@@ -360,18 +360,3 @@ if __name__ == "__main__":
360
360
  print("report is a {}.".format(type(report)))
361
361
  for item in report:
362
362
  print(item)
363
- num_errors = get_num_markup_errors(report)
364
- # how many html files in project directory?
365
- html_files = get_html_file_names()
366
-
367
- # test getting number of errorsz
368
- print(get_num_errors(report))
369
-
370
- css_code = clerk.file_to_string("tests/test_files/css_for_testing.css")
371
- css_validator_results = validate_css(css_code)
372
- is_valid = is_css_valid(css_validator_results)
373
- print(is_valid)
374
- css_validator_results = validate_css("p } color: #336699; }")
375
- print(css_validator_results)
376
- is_valid = is_css_valid(css_validator_results)
377
- print(is_valid)
File without changes
File without changes