webcode-tk 1.2.11__tar.gz → 1.2.17__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.
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/PKG-INFO +4 -3
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/pyproject.toml +4 -2
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/__init__.py +1 -1
- webcode_tk-1.2.17/webcode_tk/_style_inheritance.py +371 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/animation_tools.py +1 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/cascade_tools.py +11 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/color_keywords.py +1 -1
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/color_tools.py +6 -1
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/contrast_tools.py +58 -395
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/css_tools.py +88 -16
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/font_tools.py +1 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/html_tools.py +1 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/ux_tools.py +1 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/LICENSE.rst +0 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/README.rst +0 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/sample_report.json +0 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/utils.py +0 -0
- {webcode_tk-1.2.11 → webcode_tk-1.2.17}/webcode_tk/validator_tools.py +0 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webcode-tk
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.17
|
|
4
4
|
Summary: A set of tools for inspecting HTML and CSS code
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE.rst
|
|
7
7
|
Keywords: html,validation,css
|
|
8
8
|
Author: hundredvisionsguy
|
|
9
9
|
Author-email: cwinikka@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -22,7 +21,9 @@ Requires-Dist: file-clerk (>=1.0.9,<2.0.0)
|
|
|
22
21
|
Requires-Dist: lxml (>=6.0.0,<7.0.0)
|
|
23
22
|
Requires-Dist: mechanicalsoup (>=1.4.0,<2.0.0)
|
|
24
23
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
|
24
|
+
Requires-Dist: pre-commit (>=4.5.0,<5.0.0)
|
|
25
25
|
Requires-Dist: py-readability-metrics (>=1.4.5,<2.0.0)
|
|
26
|
+
Requires-Dist: pytest (>=9.0.3,<10.0.0)
|
|
26
27
|
Requires-Dist: pytest-mock (>=3.15.1,<4.0.0)
|
|
27
28
|
Requires-Dist: requests (>=2.28.1,<3.0.0)
|
|
28
29
|
Requires-Dist: textatistic (>=0.0.1,<0.0.2)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "webcode-tk"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.17"
|
|
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", ]
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
|
-
python = ">=3.
|
|
19
|
+
python = ">=3.10,<4.0"
|
|
20
20
|
bs4 = "^0.0.1"
|
|
21
21
|
file-clerk = "^1.0.9"
|
|
22
22
|
requests = "^2.28.1"
|
|
@@ -27,6 +27,8 @@ tinycss2 = "^1.4.0"
|
|
|
27
27
|
lxml = "^6.0.0"
|
|
28
28
|
mechanicalsoup = "^1.4.0"
|
|
29
29
|
pytest-mock = "^3.15.1"
|
|
30
|
+
pre-commit = "^4.5.0"
|
|
31
|
+
pytest = "^9.0.3"
|
|
30
32
|
|
|
31
33
|
[tool.poetry.group.dev.dependencies]
|
|
32
34
|
pytest = "*"
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_style_inheritance.py
|
|
3
|
+
By Chris Winikka
|
|
4
|
+
|
|
5
|
+
Internal utiliities for computing CSS inheritance.
|
|
6
|
+
|
|
7
|
+
This module provides optimized functions for computing CSS inheritance and
|
|
8
|
+
visual background inheritance during contrast analysis. It is used internally
|
|
9
|
+
by contrast_tools and is not part of the public API.
|
|
10
|
+
|
|
11
|
+
Functions:
|
|
12
|
+
apply_css_inheritance: Apply inheritable CSS properties top-down through
|
|
13
|
+
DOM.
|
|
14
|
+
apply_visual_background_inheritance: Propagate background colors via
|
|
15
|
+
ancestry.
|
|
16
|
+
find_ancestor_background: Locate first ancestor with explicit background.
|
|
17
|
+
apply_inheritance: Wrapper combining CSS and visual inheritance.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
|
|
22
|
+
from bs4 import Tag
|
|
23
|
+
|
|
24
|
+
DEFAULT_GLOBAL_BACKGROUND = "#ffffff"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def apply_css_inheritance(computed_styles: dict) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Apply CSS inheritance top-down (parent to children) instead of bottom-up.
|
|
30
|
+
|
|
31
|
+
This is MUCH faster than walking up the tree for each element.
|
|
32
|
+
O(n) instead of O(n*m*h) where n=elements, m=properties, h=tree_depth
|
|
33
|
+
"""
|
|
34
|
+
inheritable_props = {"color", "font-size", "font-weight"}
|
|
35
|
+
|
|
36
|
+
# Build parent-child relationships
|
|
37
|
+
parent_to_children = {}
|
|
38
|
+
child_to_parent = {}
|
|
39
|
+
|
|
40
|
+
for element in computed_styles:
|
|
41
|
+
parent = element.parent
|
|
42
|
+
if parent and parent in computed_styles:
|
|
43
|
+
if parent not in parent_to_children:
|
|
44
|
+
parent_to_children[parent] = []
|
|
45
|
+
parent_to_children[parent].append(element)
|
|
46
|
+
child_to_parent[element] = parent
|
|
47
|
+
|
|
48
|
+
# Process top-down: parent values flow to children
|
|
49
|
+
visited = set()
|
|
50
|
+
|
|
51
|
+
def traverse_tree(element, inherited_props_from_ancestors):
|
|
52
|
+
"""DFS traversal pushing inherited values down."""
|
|
53
|
+
if element in visited:
|
|
54
|
+
return
|
|
55
|
+
visited.add(element)
|
|
56
|
+
|
|
57
|
+
if element not in computed_styles:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
element_styles = computed_styles[element]
|
|
61
|
+
props_for_children = inherited_props_from_ancestors.copy()
|
|
62
|
+
|
|
63
|
+
# For each inheritable property
|
|
64
|
+
for prop in inheritable_props:
|
|
65
|
+
current_prop = element_styles.get(prop)
|
|
66
|
+
|
|
67
|
+
# If element has explicit rule, use it (don't inherit)
|
|
68
|
+
if current_prop and current_prop.get("source") == "rule":
|
|
69
|
+
props_for_children[prop] = current_prop
|
|
70
|
+
# Otherwise inherit from ancestor if available
|
|
71
|
+
elif prop in inherited_props_from_ancestors:
|
|
72
|
+
inherited_value = inherited_props_from_ancestors[prop]
|
|
73
|
+
element_styles[prop] = {
|
|
74
|
+
"value": inherited_value["value"],
|
|
75
|
+
"specificity": inherited_value["specificity"],
|
|
76
|
+
"source": "inheritance",
|
|
77
|
+
"selector": inherited_value.get("selector", "inherited"),
|
|
78
|
+
"css_file": inherited_value.get("css_file"),
|
|
79
|
+
"css_source_type": inherited_value.get("css_source_type"),
|
|
80
|
+
"inherited_from": child_to_parent.get(element),
|
|
81
|
+
}
|
|
82
|
+
# Also pass inherited value to children
|
|
83
|
+
props_for_children[prop] = element_styles[prop]
|
|
84
|
+
|
|
85
|
+
# Recurse to children with updated inherited values
|
|
86
|
+
if element in parent_to_children:
|
|
87
|
+
for child in parent_to_children[element]:
|
|
88
|
+
traverse_tree(child, props_for_children)
|
|
89
|
+
|
|
90
|
+
# Start traversal from root elements (with no parent in computed_styles)
|
|
91
|
+
for element in computed_styles:
|
|
92
|
+
if element not in child_to_parent:
|
|
93
|
+
# This is a root element
|
|
94
|
+
traverse_tree(element, {})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def apply_visual_background_inheritance(computed_styles: dict) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Apply visual background colors to elements without explicit backgrounds.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
computed_styles: a dictionary of elements with computed styles.
|
|
103
|
+
Returns:
|
|
104
|
+
None
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
for element in computed_styles:
|
|
108
|
+
element_styles = computed_styles[element]
|
|
109
|
+
|
|
110
|
+
# Check for BOTH background-color AND background
|
|
111
|
+
current_bg = element_styles.get("background-color")
|
|
112
|
+
has_background_shorthand = "background" in element_styles
|
|
113
|
+
|
|
114
|
+
# If element has background shorthand, process it for images
|
|
115
|
+
if has_background_shorthand:
|
|
116
|
+
bg_value = element_styles["background"]["value"]
|
|
117
|
+
|
|
118
|
+
# Check if background contains a raster image
|
|
119
|
+
if contains_raster_image(bg_value):
|
|
120
|
+
# Mark as contrast-indeterminate - cannot analyze
|
|
121
|
+
element_styles["background-color"] = {
|
|
122
|
+
"value": None,
|
|
123
|
+
"specificity": "000",
|
|
124
|
+
"contrast_analysis": "indeterminate",
|
|
125
|
+
"reason": "background_image_blocks_color_analysis",
|
|
126
|
+
"original_background": bg_value,
|
|
127
|
+
"visual_inheritance": False,
|
|
128
|
+
}
|
|
129
|
+
continue
|
|
130
|
+
else:
|
|
131
|
+
# Has usable background shorthand - skip inheritance
|
|
132
|
+
continue
|
|
133
|
+
if not current_bg:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
bg_source = current_bg.get("source")
|
|
137
|
+
bg_value = current_bg.get("value")
|
|
138
|
+
|
|
139
|
+
# Skip elements with explicit CSS rules
|
|
140
|
+
if bg_source == "rule":
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Only update elements that still have the default white background
|
|
144
|
+
# This prevents overwriting elements with existing proper backgrounds
|
|
145
|
+
if bg_source != "default":
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Element needs background inheritance (lines 1120-1141)
|
|
149
|
+
ancestor_bg = find_ancestor_background(element, computed_styles)
|
|
150
|
+
|
|
151
|
+
if not ancestor_bg or ancestor_bg.get("source") == "default":
|
|
152
|
+
# No explicit background to inherit - keep browser default
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if ancestor_bg.get("contrast_analysis") == "indeterminate":
|
|
156
|
+
# Ancestor is indeterminate - propagate that status
|
|
157
|
+
element_styles["background-color"] = {
|
|
158
|
+
"value": None,
|
|
159
|
+
"specificity": "000",
|
|
160
|
+
"contrast_analysis": "indeterminate",
|
|
161
|
+
"reason": ancestor_bg["reason"],
|
|
162
|
+
"inherited_from": ancestor_bg["source_element"],
|
|
163
|
+
"source": "visual_inheritance",
|
|
164
|
+
"original_background": ancestor_bg.get("original_background"),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
# Normal inheritance - extract usable color
|
|
169
|
+
contrast_color = extract_contrast_color(ancestor_bg["value"])
|
|
170
|
+
effective_color = (
|
|
171
|
+
contrast_color if contrast_color else ancestor_bg["value"]
|
|
172
|
+
)
|
|
173
|
+
element_styles["background-color"] = {
|
|
174
|
+
"value": effective_color,
|
|
175
|
+
"specificity": "000",
|
|
176
|
+
"source": "visual_inheritance",
|
|
177
|
+
"inherited_from": ancestor_bg["source_element"],
|
|
178
|
+
"contrast_analysis": "determinable",
|
|
179
|
+
"original_background": (
|
|
180
|
+
ancestor_bg["value"]
|
|
181
|
+
if ancestor_bg["value"] != DEFAULT_GLOBAL_BACKGROUND
|
|
182
|
+
else None
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def contains_raster_image(background_value: str) -> bool:
|
|
188
|
+
"""Check if background contains a raster image that blocks color analysis.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
background_value: the css value applied to the background property.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
bool: whether the background value includes a raster image or not.
|
|
195
|
+
"""
|
|
196
|
+
# Add None check
|
|
197
|
+
if background_value is None:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
return bool(
|
|
201
|
+
re.search(
|
|
202
|
+
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def extract_contrast_color(background_value: str) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Extract usable color for contrast analysis, or None.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
background_value (str): CSS background property value.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
str: Color value suitable for contrast analysis, or None if no
|
|
216
|
+
usable color found.
|
|
217
|
+
"""
|
|
218
|
+
# Add None check at the beginning
|
|
219
|
+
if background_value is None:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# Remove raster images - they block color visibility
|
|
223
|
+
if re.search(
|
|
224
|
+
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
225
|
+
):
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
# Handle gradients - extract representative color
|
|
229
|
+
if "gradient" in background_value.lower():
|
|
230
|
+
return extract_gradient_contrast_color(background_value)
|
|
231
|
+
|
|
232
|
+
# Handle solid colors
|
|
233
|
+
re_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|"
|
|
234
|
+
re_pattern += r"hsla\([^)]+\)|[a-z]+)"
|
|
235
|
+
color_match = re.search(
|
|
236
|
+
re_pattern,
|
|
237
|
+
background_value,
|
|
238
|
+
re.I,
|
|
239
|
+
)
|
|
240
|
+
return color_match.group(1) if color_match else None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def find_ancestor_background(element: Tag, computed_styles: dict) -> dict:
|
|
244
|
+
"""
|
|
245
|
+
Walk up DOM tree to find first ancestor with background.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
element (Tag): BeautifulSoup Tag object to start searching from.
|
|
249
|
+
computed_styles (dict): Dictionary mapping elements to their
|
|
250
|
+
computed styles.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
dict: Dictionary containing 'value' (background color) and
|
|
254
|
+
'source_element' (ancestor element or None).
|
|
255
|
+
"""
|
|
256
|
+
current = element.parent
|
|
257
|
+
|
|
258
|
+
while current:
|
|
259
|
+
if current in computed_styles:
|
|
260
|
+
current_styles = computed_styles[current]
|
|
261
|
+
|
|
262
|
+
# Flag to track if we should skip this ancestor
|
|
263
|
+
skip_ancestor = False
|
|
264
|
+
|
|
265
|
+
# Check for background-color or background
|
|
266
|
+
for bg_prop in ["background-color", "background"]:
|
|
267
|
+
if bg_prop in current_styles:
|
|
268
|
+
bg_source = current_styles[bg_prop].get("source")
|
|
269
|
+
|
|
270
|
+
# Skip elements that only have visual inheritance
|
|
271
|
+
if bg_source == "visual_inheritance":
|
|
272
|
+
skip_ancestor = True
|
|
273
|
+
break # ✅ Break out of property loop
|
|
274
|
+
|
|
275
|
+
# Accept both "rule" and "default" sources
|
|
276
|
+
if bg_source in ["rule", "default"]:
|
|
277
|
+
bg_value = current_styles[bg_prop]["value"]
|
|
278
|
+
if bg_source == "default":
|
|
279
|
+
# Skip ancestors with only default backgrounds
|
|
280
|
+
skip_ancestor = True
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
# Does this ancestor already is indeterminate status
|
|
284
|
+
if (
|
|
285
|
+
current_styles[bg_prop].get("contrast_analysis")
|
|
286
|
+
== "indeterminate"
|
|
287
|
+
):
|
|
288
|
+
return {
|
|
289
|
+
"value": None,
|
|
290
|
+
"source_element": current,
|
|
291
|
+
"contrast_analysis": "indeterminate",
|
|
292
|
+
"reason": "ancestor_has_background_image",
|
|
293
|
+
"original_background": current_styles[
|
|
294
|
+
bg_prop
|
|
295
|
+
].get("original_background", bg_value),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Skip None values and continue searching
|
|
299
|
+
if bg_value is None:
|
|
300
|
+
continue # Continue to next property
|
|
301
|
+
|
|
302
|
+
# Check if this background contains a raster image
|
|
303
|
+
if contains_raster_image(bg_value):
|
|
304
|
+
return {
|
|
305
|
+
"value": None,
|
|
306
|
+
"source_element": current,
|
|
307
|
+
"contrast_analysis": "indeterminate",
|
|
308
|
+
"reason": "ancestor_has_background_image",
|
|
309
|
+
"original_background": bg_value,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Found explicit background
|
|
313
|
+
bg_items = current_styles[bg_prop].items()
|
|
314
|
+
return {
|
|
315
|
+
"value": bg_value,
|
|
316
|
+
"source_element": current,
|
|
317
|
+
"contrast_analysis": "determinable",
|
|
318
|
+
# Copy metadata from ancestor
|
|
319
|
+
**{
|
|
320
|
+
metadata_key: metadata_value
|
|
321
|
+
for metadata_key, metadata_value in bg_items
|
|
322
|
+
if metadata_key not in ["value"]
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# If we flagged this ancestor to skip, move to next ancestor
|
|
327
|
+
if skip_ancestor:
|
|
328
|
+
current = current.parent
|
|
329
|
+
continue # ✅ Now this continues to next ancestor
|
|
330
|
+
|
|
331
|
+
current = current.parent
|
|
332
|
+
|
|
333
|
+
# No ancestor background found, use default
|
|
334
|
+
return {
|
|
335
|
+
"value": DEFAULT_GLOBAL_BACKGROUND,
|
|
336
|
+
"source_element": None,
|
|
337
|
+
"contrast_analysis": "determinable",
|
|
338
|
+
"source": "default",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def extract_gradient_contrast_color(background_value: str) -> str:
|
|
343
|
+
"""
|
|
344
|
+
Extract representative color from gradient for contrast analysis.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
background_value (str): CSS gradient value (e.g.,
|
|
348
|
+
'linear-gradient(red, blue)').
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
str: Representative color for contrast analysis, or None if no
|
|
352
|
+
color found.
|
|
353
|
+
"""
|
|
354
|
+
# Add None check
|
|
355
|
+
if background_value is None:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# Strategy: Use the final color in gradient (most visible for reading)
|
|
359
|
+
# This handles: linear-gradient(red, blue) -> blue
|
|
360
|
+
# radial-gradient(center, red, blue) -> blue
|
|
361
|
+
|
|
362
|
+
# Find all colors in gradient
|
|
363
|
+
color_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|"
|
|
364
|
+
color_pattern += r"hsl\([^)]+\)|hsla\([^)]+\)|[a-z]+)"
|
|
365
|
+
colors = re.findall(color_pattern, background_value, re.I)
|
|
366
|
+
|
|
367
|
+
if colors:
|
|
368
|
+
# Use last color (end of gradient)
|
|
369
|
+
return colors[-1]
|
|
370
|
+
|
|
371
|
+
return None
|
|
@@ -8,6 +8,7 @@ all styles applied to every element on the page.
|
|
|
8
8
|
With that, it scans each stylesheet and applies each style one at a time
|
|
9
9
|
to all elements and their children (if applicable) of a page.
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
import ctypes
|
|
12
13
|
import re
|
|
13
14
|
from collections import abc
|
|
@@ -1539,6 +1540,16 @@ def targets_element_directly(element: Element, selector: str) -> bool:
|
|
|
1539
1540
|
elif selector_type == "id_selector":
|
|
1540
1541
|
attributes = element.attributes
|
|
1541
1542
|
if attributes:
|
|
1543
|
+
if not isinstance(attributes, dict):
|
|
1544
|
+
try:
|
|
1545
|
+
attributes = dict(attributes)
|
|
1546
|
+
except Exception:
|
|
1547
|
+
msg = "This is not a dictionary and cannot be converted"
|
|
1548
|
+
msg += " into one!"
|
|
1549
|
+
raise ValueError(msg)
|
|
1550
|
+
else:
|
|
1551
|
+
data_type = str(type(attributes))
|
|
1552
|
+
print(data_type)
|
|
1542
1553
|
id_attribute = attributes.get("id")
|
|
1543
1554
|
if id_attribute:
|
|
1544
1555
|
targets = selector in attributes.get("id")
|
|
@@ -19,6 +19,7 @@ One thing led to another, yada yada, then I realized that this could
|
|
|
19
19
|
be a useful tool in my web grading projects, and there you have it:
|
|
20
20
|
`color_tools`.
|
|
21
21
|
"""
|
|
22
|
+
|
|
22
23
|
import re
|
|
23
24
|
|
|
24
25
|
from webcode_tk import color_keywords
|
|
@@ -313,7 +314,7 @@ def has_alpha_channel(code: str) -> bool:
|
|
|
313
314
|
"""
|
|
314
315
|
has_alpha = False
|
|
315
316
|
if "#" in code:
|
|
316
|
-
if len(code) == 9:
|
|
317
|
+
if len(code) == 9 or len(code) == 5:
|
|
317
318
|
has_alpha = True
|
|
318
319
|
if "hsla(" in code:
|
|
319
320
|
has_alpha = True
|
|
@@ -321,6 +322,10 @@ def has_alpha_channel(code: str) -> bool:
|
|
|
321
322
|
has_alpha = True
|
|
322
323
|
if "rgba(" in code:
|
|
323
324
|
has_alpha = True
|
|
325
|
+
if "rgb" in code and "/" in code:
|
|
326
|
+
has_alpha = True
|
|
327
|
+
if "transparent" in code:
|
|
328
|
+
has_alpha = True
|
|
324
329
|
return has_alpha
|
|
325
330
|
|
|
326
331
|
|
|
@@ -31,6 +31,7 @@ Constants:
|
|
|
31
31
|
mathematical contrast ratios and size thresholds that determine WCAG
|
|
32
32
|
compliance.
|
|
33
33
|
"""
|
|
34
|
+
|
|
34
35
|
import copy
|
|
35
36
|
import re
|
|
36
37
|
|
|
@@ -40,6 +41,13 @@ from bs4 import NavigableString
|
|
|
40
41
|
from bs4 import Tag
|
|
41
42
|
from file_clerk import clerk
|
|
42
43
|
|
|
44
|
+
from webcode_tk._style_inheritance import (
|
|
45
|
+
apply_css_inheritance,
|
|
46
|
+
apply_visual_background_inheritance,
|
|
47
|
+
contains_raster_image,
|
|
48
|
+
DEFAULT_GLOBAL_BACKGROUND,
|
|
49
|
+
extract_contrast_color,
|
|
50
|
+
)
|
|
43
51
|
from webcode_tk import color_tools
|
|
44
52
|
from webcode_tk import css_tools
|
|
45
53
|
from webcode_tk import font_tools
|
|
@@ -49,7 +57,6 @@ from webcode_tk.font_tools import split_value_unit
|
|
|
49
57
|
|
|
50
58
|
# Browser default styling constants
|
|
51
59
|
DEFAULT_GLOBAL_COLOR = "#000000"
|
|
52
|
-
DEFAULT_GLOBAL_BACKGROUND = "#ffffff"
|
|
53
60
|
DEFAULT_LINK_COLOR = "#0000EE"
|
|
54
61
|
DEFAULT_LINK_VISITED = "#551A8B"
|
|
55
62
|
ROOT_FONT_SIZE = 16
|
|
@@ -166,20 +173,6 @@ def generate_contrast_report(
|
|
|
166
173
|
for file in project_files:
|
|
167
174
|
errors = project_files.get(file)
|
|
168
175
|
|
|
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
176
|
# Now append to report
|
|
184
177
|
if errors:
|
|
185
178
|
if "has no CSS" in errors[0]:
|
|
@@ -885,12 +878,16 @@ def apply_rule_to_element(
|
|
|
885
878
|
"specificity": rule_specificity,
|
|
886
879
|
"source": "rule",
|
|
887
880
|
"selector": selector,
|
|
888
|
-
"css_file":
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
881
|
+
"css_file": (
|
|
882
|
+
css_source_info.get("filename")
|
|
883
|
+
if css_source_info
|
|
884
|
+
else None
|
|
885
|
+
),
|
|
886
|
+
"css_source_type": (
|
|
887
|
+
css_source_info.get("source_type")
|
|
888
|
+
if css_source_info
|
|
889
|
+
else None
|
|
890
|
+
),
|
|
894
891
|
}
|
|
895
892
|
|
|
896
893
|
# Check if we should apply this font-size rule
|
|
@@ -925,12 +922,16 @@ def apply_rule_to_element(
|
|
|
925
922
|
"specificity": rule_specificity,
|
|
926
923
|
"source": "rule",
|
|
927
924
|
"selector": selector,
|
|
928
|
-
"css_file":
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
925
|
+
"css_file": (
|
|
926
|
+
css_source_info.get("filename")
|
|
927
|
+
if css_source_info
|
|
928
|
+
else None
|
|
929
|
+
),
|
|
930
|
+
"css_source_type": (
|
|
931
|
+
css_source_info.get("source_type")
|
|
932
|
+
if css_source_info
|
|
933
|
+
else None
|
|
934
|
+
),
|
|
934
935
|
"contrast_analysis": "indeterminate",
|
|
935
936
|
"reason": "background_image_blocks_color_analysis",
|
|
936
937
|
"original_background": property_value,
|
|
@@ -947,16 +948,22 @@ def apply_rule_to_element(
|
|
|
947
948
|
"specificity": rule_specificity,
|
|
948
949
|
"source": "rule",
|
|
949
950
|
"selector": selector,
|
|
950
|
-
"css_file":
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
951
|
+
"css_file": (
|
|
952
|
+
css_source_info.get("filename")
|
|
953
|
+
if css_source_info
|
|
954
|
+
else None
|
|
955
|
+
),
|
|
956
|
+
"css_source_type": (
|
|
957
|
+
css_source_info.get("source_type")
|
|
958
|
+
if css_source_info
|
|
959
|
+
else None
|
|
960
|
+
),
|
|
956
961
|
"contrast_analysis": "determinable",
|
|
957
|
-
"original_background":
|
|
958
|
-
|
|
959
|
-
|
|
962
|
+
"original_background": (
|
|
963
|
+
property_value
|
|
964
|
+
if property_value != effective_color
|
|
965
|
+
else None
|
|
966
|
+
),
|
|
960
967
|
}
|
|
961
968
|
|
|
962
969
|
# Apply specificity logic and set the property
|
|
@@ -1014,12 +1021,16 @@ def apply_rule_to_element(
|
|
|
1014
1021
|
"specificity": rule_specificity,
|
|
1015
1022
|
"source": "rule",
|
|
1016
1023
|
"selector": selector,
|
|
1017
|
-
"css_file":
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1024
|
+
"css_file": (
|
|
1025
|
+
css_source_info.get("filename")
|
|
1026
|
+
if css_source_info
|
|
1027
|
+
else None
|
|
1028
|
+
),
|
|
1029
|
+
"css_source_type": (
|
|
1030
|
+
css_source_info.get("source_type")
|
|
1031
|
+
if css_source_info
|
|
1032
|
+
else None
|
|
1033
|
+
),
|
|
1023
1034
|
}
|
|
1024
1035
|
|
|
1025
1036
|
return
|
|
@@ -1141,149 +1152,6 @@ def apply_inheritance(soup: BeautifulSoup, computed_styles: dict) -> None:
|
|
|
1141
1152
|
apply_visual_background_inheritance(computed_styles)
|
|
1142
1153
|
|
|
1143
1154
|
|
|
1144
|
-
def apply_css_inheritance(computed_styles: dict) -> None:
|
|
1145
|
-
"""
|
|
1146
|
-
Apply traditional CSS inheritance for inheritable properties.
|
|
1147
|
-
Only inherit from the closest ancestor with an explicit rule.
|
|
1148
|
-
"""
|
|
1149
|
-
inheritable_props = {"color", "font-size", "font-weight"}
|
|
1150
|
-
|
|
1151
|
-
for child_element in computed_styles:
|
|
1152
|
-
child_styles = computed_styles[child_element]
|
|
1153
|
-
|
|
1154
|
-
# find closest ancestor with explicit value
|
|
1155
|
-
for prop in inheritable_props:
|
|
1156
|
-
child_prop = child_styles.get(prop)
|
|
1157
|
-
|
|
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
|
-
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def apply_visual_background_inheritance(computed_styles: dict) -> None:
|
|
1198
|
-
"""
|
|
1199
|
-
Apply visual background colors to elements without explicit backgrounds.
|
|
1200
|
-
|
|
1201
|
-
Args:
|
|
1202
|
-
computed_styles: a dictionary of elements with computed styles.
|
|
1203
|
-
Returns:
|
|
1204
|
-
None
|
|
1205
|
-
"""
|
|
1206
|
-
|
|
1207
|
-
for element in computed_styles:
|
|
1208
|
-
element_styles = computed_styles[element]
|
|
1209
|
-
|
|
1210
|
-
# Check for BOTH background-color AND background
|
|
1211
|
-
current_bg = element_styles.get("background-color")
|
|
1212
|
-
has_background_shorthand = "background" in element_styles
|
|
1213
|
-
|
|
1214
|
-
# If element has background shorthand, process it for images
|
|
1215
|
-
if has_background_shorthand:
|
|
1216
|
-
bg_value = element_styles["background"]["value"]
|
|
1217
|
-
|
|
1218
|
-
# Check if background contains a raster image
|
|
1219
|
-
if contains_raster_image(bg_value):
|
|
1220
|
-
# Mark as contrast-indeterminate - cannot analyze
|
|
1221
|
-
element_styles["background-color"] = {
|
|
1222
|
-
"value": None,
|
|
1223
|
-
"specificity": "000",
|
|
1224
|
-
"contrast_analysis": "indeterminate",
|
|
1225
|
-
"reason": "background_image_blocks_color_analysis",
|
|
1226
|
-
"original_background": bg_value,
|
|
1227
|
-
"visual_inheritance": False,
|
|
1228
|
-
}
|
|
1229
|
-
continue
|
|
1230
|
-
else:
|
|
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
|
-
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
1155
|
def has_usable_background_color(background_prop: dict) -> bool:
|
|
1288
1156
|
"""
|
|
1289
1157
|
Check if background property contains usable color for contrast.
|
|
@@ -1313,193 +1181,6 @@ def has_usable_background_color(background_prop: dict) -> bool:
|
|
|
1313
1181
|
)
|
|
1314
1182
|
|
|
1315
1183
|
|
|
1316
|
-
def contains_raster_image(background_value: str) -> bool:
|
|
1317
|
-
"""Check if background contains a raster image that blocks color analysis.
|
|
1318
|
-
|
|
1319
|
-
Args:
|
|
1320
|
-
background_value: the css value applied to the background property.
|
|
1321
|
-
|
|
1322
|
-
Returns:
|
|
1323
|
-
bool: whether the background value includes a raster image or not.
|
|
1324
|
-
"""
|
|
1325
|
-
# Add None check
|
|
1326
|
-
if background_value is None:
|
|
1327
|
-
return False
|
|
1328
|
-
|
|
1329
|
-
return bool(
|
|
1330
|
-
re.search(
|
|
1331
|
-
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
1332
|
-
)
|
|
1333
|
-
)
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
def extract_contrast_color(background_value: str) -> str:
|
|
1337
|
-
"""
|
|
1338
|
-
Extract usable color for contrast analysis, or None.
|
|
1339
|
-
|
|
1340
|
-
Args:
|
|
1341
|
-
background_value (str): CSS background property value.
|
|
1342
|
-
|
|
1343
|
-
Returns:
|
|
1344
|
-
str: Color value suitable for contrast analysis, or None if no
|
|
1345
|
-
usable color found.
|
|
1346
|
-
"""
|
|
1347
|
-
# Add None check at the beginning
|
|
1348
|
-
if background_value is None:
|
|
1349
|
-
return None
|
|
1350
|
-
|
|
1351
|
-
# Remove raster images - they block color visibility
|
|
1352
|
-
if re.search(
|
|
1353
|
-
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
1354
|
-
):
|
|
1355
|
-
return None
|
|
1356
|
-
|
|
1357
|
-
# Handle gradients - extract representative color
|
|
1358
|
-
if "gradient" in background_value.lower():
|
|
1359
|
-
return extract_gradient_contrast_color(background_value)
|
|
1360
|
-
|
|
1361
|
-
# Handle solid colors
|
|
1362
|
-
re_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|"
|
|
1363
|
-
re_pattern += r"hsla\([^)]+\)|[a-z]+)"
|
|
1364
|
-
color_match = re.search(
|
|
1365
|
-
re_pattern,
|
|
1366
|
-
background_value,
|
|
1367
|
-
re.I,
|
|
1368
|
-
)
|
|
1369
|
-
return color_match.group(1) if color_match else None
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
def extract_gradient_contrast_color(background_value: str) -> str:
|
|
1373
|
-
"""
|
|
1374
|
-
Extract representative color from gradient for contrast analysis.
|
|
1375
|
-
|
|
1376
|
-
Args:
|
|
1377
|
-
background_value (str): CSS gradient value (e.g.,
|
|
1378
|
-
'linear-gradient(red, blue)').
|
|
1379
|
-
|
|
1380
|
-
Returns:
|
|
1381
|
-
str: Representative color for contrast analysis, or None if no
|
|
1382
|
-
color found.
|
|
1383
|
-
"""
|
|
1384
|
-
# Add None check
|
|
1385
|
-
if background_value is None:
|
|
1386
|
-
return None
|
|
1387
|
-
|
|
1388
|
-
# Strategy: Use the final color in gradient (most visible for reading)
|
|
1389
|
-
# This handles: linear-gradient(red, blue) -> blue
|
|
1390
|
-
# radial-gradient(center, red, blue) -> blue
|
|
1391
|
-
|
|
1392
|
-
# Find all colors in gradient
|
|
1393
|
-
color_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|"
|
|
1394
|
-
color_pattern += r"hsl\([^)]+\)|hsla\([^)]+\)|[a-z]+)"
|
|
1395
|
-
colors = re.findall(color_pattern, background_value, re.I)
|
|
1396
|
-
|
|
1397
|
-
if colors:
|
|
1398
|
-
# Use last color (end of gradient)
|
|
1399
|
-
return colors[-1]
|
|
1400
|
-
|
|
1401
|
-
return None
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
def find_ancestor_background(element: Tag, computed_styles: dict) -> dict:
|
|
1405
|
-
"""
|
|
1406
|
-
Walk up DOM tree to find first ancestor with background.
|
|
1407
|
-
|
|
1408
|
-
Args:
|
|
1409
|
-
element (Tag): BeautifulSoup Tag object to start searching from.
|
|
1410
|
-
computed_styles (dict): Dictionary mapping elements to their
|
|
1411
|
-
computed styles.
|
|
1412
|
-
|
|
1413
|
-
Returns:
|
|
1414
|
-
dict: Dictionary containing 'value' (background color) and
|
|
1415
|
-
'source_element' (ancestor element or None).
|
|
1416
|
-
"""
|
|
1417
|
-
current = element.parent
|
|
1418
|
-
|
|
1419
|
-
while current:
|
|
1420
|
-
if current in computed_styles:
|
|
1421
|
-
current_styles = computed_styles[current]
|
|
1422
|
-
|
|
1423
|
-
# Flag to track if we should skip this ancestor
|
|
1424
|
-
skip_ancestor = False
|
|
1425
|
-
|
|
1426
|
-
# Check for background-color or background
|
|
1427
|
-
for bg_prop in ["background-color", "background"]:
|
|
1428
|
-
if bg_prop in current_styles:
|
|
1429
|
-
bg_source = current_styles[bg_prop].get("source")
|
|
1430
|
-
|
|
1431
|
-
# Skip elements that only have visual inheritance
|
|
1432
|
-
if bg_source == "visual_inheritance":
|
|
1433
|
-
skip_ancestor = True
|
|
1434
|
-
break # ✅ Break out of property loop
|
|
1435
|
-
|
|
1436
|
-
# Accept both "rule" and "default" sources
|
|
1437
|
-
if bg_source in ["rule", "default"]:
|
|
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
|
|
1443
|
-
|
|
1444
|
-
# Does this ancestor already is indeterminate status
|
|
1445
|
-
if (
|
|
1446
|
-
current_styles[bg_prop].get("contrast_analysis")
|
|
1447
|
-
== "indeterminate"
|
|
1448
|
-
):
|
|
1449
|
-
return {
|
|
1450
|
-
"value": None,
|
|
1451
|
-
"source_element": current,
|
|
1452
|
-
"contrast_analysis": "indeterminate",
|
|
1453
|
-
"reason": "ancestor_has_background_image",
|
|
1454
|
-
"original_background": current_styles[
|
|
1455
|
-
bg_prop
|
|
1456
|
-
].get("original_background", bg_value),
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
# Skip None values and continue searching
|
|
1460
|
-
if bg_value is None:
|
|
1461
|
-
continue # Continue to next property
|
|
1462
|
-
|
|
1463
|
-
# Check if this background contains a raster image
|
|
1464
|
-
if contains_raster_image(bg_value):
|
|
1465
|
-
return {
|
|
1466
|
-
"value": None,
|
|
1467
|
-
"source_element": current,
|
|
1468
|
-
"contrast_analysis": "indeterminate",
|
|
1469
|
-
"reason": "ancestor_has_background_image",
|
|
1470
|
-
"original_background": bg_value,
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
# Found explicit background
|
|
1474
|
-
bg_items = current_styles[bg_prop].items()
|
|
1475
|
-
return {
|
|
1476
|
-
"value": bg_value,
|
|
1477
|
-
"source_element": current,
|
|
1478
|
-
"contrast_analysis": "determinable",
|
|
1479
|
-
# Copy metadata from ancestor
|
|
1480
|
-
**{
|
|
1481
|
-
metadata_key: metadata_value
|
|
1482
|
-
for metadata_key, metadata_value in bg_items
|
|
1483
|
-
if metadata_key not in ["value"]
|
|
1484
|
-
},
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
# If we flagged this ancestor to skip, move to next ancestor
|
|
1488
|
-
if skip_ancestor:
|
|
1489
|
-
current = current.parent
|
|
1490
|
-
continue # ✅ Now this continues to next ancestor
|
|
1491
|
-
|
|
1492
|
-
current = current.parent
|
|
1493
|
-
|
|
1494
|
-
# No ancestor background found, use default
|
|
1495
|
-
return {
|
|
1496
|
-
"value": DEFAULT_GLOBAL_BACKGROUND,
|
|
1497
|
-
"source_element": None,
|
|
1498
|
-
"contrast_analysis": "determinable",
|
|
1499
|
-
"source": "default",
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
1184
|
def is_inheritable_property(property_name: str) -> bool:
|
|
1504
1185
|
"""
|
|
1505
1186
|
Determines if a CSS property is inheritable by default.
|
|
@@ -1557,25 +1238,6 @@ def analyze_elements_for_contrast(
|
|
|
1557
1238
|
"contrast_analysis"
|
|
1558
1239
|
)
|
|
1559
1240
|
|
|
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
|
-
|
|
1579
1241
|
if is_large:
|
|
1580
1242
|
aaa_results = contrast_report.get("Large AAA")
|
|
1581
1243
|
aa_results = contrast_report.get("Large AA")
|
|
@@ -1607,7 +1269,6 @@ def analyze_elements_for_contrast(
|
|
|
1607
1269
|
"wcag_aa_pass": aa_results,
|
|
1608
1270
|
"wcag_aaa_pass": aaa_results,
|
|
1609
1271
|
"contrast_analysis": contrast_analysis,
|
|
1610
|
-
"global_color_failure": is_global_failure,
|
|
1611
1272
|
}
|
|
1612
1273
|
|
|
1613
1274
|
# add to results
|
|
@@ -1684,9 +1345,11 @@ def extract_property_source(property_data: dict) -> dict:
|
|
|
1684
1345
|
"source_type": "visual_inheritance",
|
|
1685
1346
|
"css_file": "visual_cascade",
|
|
1686
1347
|
"selector": "ancestor_background",
|
|
1687
|
-
"inherited_from":
|
|
1688
|
-
|
|
1689
|
-
|
|
1348
|
+
"inherited_from": (
|
|
1349
|
+
getattr(inherited_element, "name", None)
|
|
1350
|
+
if inherited_element
|
|
1351
|
+
else None
|
|
1352
|
+
),
|
|
1690
1353
|
}
|
|
1691
1354
|
elif source_type == "rule":
|
|
1692
1355
|
return {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
This module is a set of tools to analyze CSS syntax as well as properties
|
|
3
3
|
and values.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import re
|
|
6
7
|
from typing import Union
|
|
7
8
|
|
|
@@ -1234,7 +1235,7 @@ def get_colors_from_gradient(gradient: str) -> list:
|
|
|
1234
1235
|
return colors
|
|
1235
1236
|
|
|
1236
1237
|
|
|
1237
|
-
def get_comment_positions(code:
|
|
1238
|
+
def get_comment_positions(code: str) -> Union[list, None]:
|
|
1238
1239
|
"""looks for index positions of first opening and closing comment.
|
|
1239
1240
|
|
|
1240
1241
|
From this function, you can create a slice of a comment from the
|
|
@@ -1383,7 +1384,11 @@ def get_global_color_details(rulesets: Union[list, tuple]) -> list:
|
|
|
1383
1384
|
global_selectors = ("html", "body", ":root", "*")
|
|
1384
1385
|
global_rulesets = []
|
|
1385
1386
|
for ruleset in rulesets:
|
|
1386
|
-
if
|
|
1387
|
+
if (
|
|
1388
|
+
ruleset.selector in global_selectors
|
|
1389
|
+
or "html" in ruleset.selector[:4]
|
|
1390
|
+
or "body" in ruleset.selector[:4]
|
|
1391
|
+
):
|
|
1387
1392
|
selector = ruleset.selector
|
|
1388
1393
|
background_color = ""
|
|
1389
1394
|
color = ""
|
|
@@ -1392,10 +1397,6 @@ def get_global_color_details(rulesets: Union[list, tuple]) -> list:
|
|
|
1392
1397
|
background_color = declaration.value
|
|
1393
1398
|
elif declaration.property == "color":
|
|
1394
1399
|
color = declaration.value
|
|
1395
|
-
if is_gradient(color):
|
|
1396
|
-
colors = process_gradient(color)
|
|
1397
|
-
todo = input("We have colors: " + colors)
|
|
1398
|
-
print(todo)
|
|
1399
1400
|
elif declaration.property == "background":
|
|
1400
1401
|
background_color = declaration.value
|
|
1401
1402
|
if is_gradient(background_color):
|
|
@@ -2452,22 +2453,93 @@ def get_global_color_report(project_dir: str, level="aaa") -> list:
|
|
|
2452
2453
|
for data in all_file_data:
|
|
2453
2454
|
filename = data[0]
|
|
2454
2455
|
passes = []
|
|
2456
|
+
global_data = {
|
|
2457
|
+
"selector": "",
|
|
2458
|
+
"specificity": "",
|
|
2459
|
+
"bg_color": "",
|
|
2460
|
+
"color": "",
|
|
2461
|
+
"contrast_ratio": 0.0,
|
|
2462
|
+
"passes_normal": False,
|
|
2463
|
+
"passes_large": False,
|
|
2464
|
+
}
|
|
2455
2465
|
for sheet in data[1]:
|
|
2456
2466
|
rules = sheet.rulesets
|
|
2457
2467
|
global_color_data = get_global_color_details(rules)
|
|
2458
2468
|
if global_color_data:
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2469
|
+
previous_selector = global_data.get("selector")
|
|
2470
|
+
if previous_selector:
|
|
2471
|
+
previous_data = global_data.copy()
|
|
2472
|
+
|
|
2473
|
+
data = global_color_data[0]
|
|
2474
|
+
current_selector = data.get("selector")
|
|
2475
|
+
global_data["selector"] = current_selector
|
|
2476
|
+
|
|
2477
|
+
current_specificity = get_specificity(current_selector)
|
|
2478
|
+
global_data["specificity"] = current_specificity
|
|
2479
|
+
|
|
2480
|
+
bg_color = data.get("background-color")
|
|
2481
|
+
global_data["bg_color"] = bg_color
|
|
2482
|
+
|
|
2483
|
+
color = data.get("color")
|
|
2484
|
+
global_data["color"] = color
|
|
2485
|
+
|
|
2486
|
+
ratio = data.get("contrast_ratio")
|
|
2487
|
+
global_data["contrast_ratio"] = ratio
|
|
2488
|
+
|
|
2489
|
+
if level == "aaa":
|
|
2490
|
+
passes_normal = data.get("passes_normal_aaa")
|
|
2491
|
+
passes_large = data.get("passes_large_aaa")
|
|
2492
|
+
else:
|
|
2493
|
+
passes_normal = data.get("passes_normal_aa")
|
|
2494
|
+
passes_large = data.get("passes_large_aa")
|
|
2495
|
+
|
|
2496
|
+
global_data["passes_normal"] = passes_normal
|
|
2497
|
+
global_data["passes_large"] = passes_large
|
|
2498
|
+
|
|
2499
|
+
# If there was a previous selector
|
|
2500
|
+
if previous_selector:
|
|
2501
|
+
previous_specificity = previous_data.get("specificity")
|
|
2502
|
+
|
|
2503
|
+
# If previous specificity is higher, we need everything
|
|
2504
|
+
# from previous
|
|
2505
|
+
if previous_specificity > current_specificity:
|
|
2506
|
+
# Keep all from previous data
|
|
2507
|
+
for key, val in previous_data.items():
|
|
2508
|
+
if key in ["bg_color", "color"] and val:
|
|
2509
|
+
global_data[key] = val
|
|
2463
2510
|
else:
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2511
|
+
# we already overrode everything
|
|
2512
|
+
# Anything missing? We need to pull from previous data
|
|
2513
|
+
for key, val in global_data.items():
|
|
2514
|
+
if not val:
|
|
2515
|
+
new_val = previous_data.get(key)
|
|
2516
|
+
if new_val:
|
|
2517
|
+
global_data[key] = new_val
|
|
2518
|
+
# reassess color contrast for global_data
|
|
2519
|
+
bg_color = global_data.get("bg_color")
|
|
2520
|
+
color = global_data.get("color")
|
|
2521
|
+
if not bg_color or not color:
|
|
2522
|
+
details = f"fail: {filename} does not apply "
|
|
2523
|
+
if not bg_color and not color:
|
|
2524
|
+
details += "neither background nor text color."
|
|
2525
|
+
elif not bg_color:
|
|
2526
|
+
details += "background color."
|
|
2527
|
+
else:
|
|
2528
|
+
details += "text color."
|
|
2469
2529
|
else:
|
|
2470
|
-
|
|
2530
|
+
bg_hex = color_tools.get_hex(bg_color)
|
|
2531
|
+
color_hex = color_tools.get_hex(color)
|
|
2532
|
+
contrast_ratio = color_tools.contrast_ratio(bg_hex, color_hex)
|
|
2533
|
+
normal_level = f"Normal {level.upper()}"
|
|
2534
|
+
passes = color_tools.passes_color_contrast(
|
|
2535
|
+
normal_level, bg_hex, color_hex
|
|
2536
|
+
)
|
|
2537
|
+
if passes:
|
|
2538
|
+
details = f"pass: {filename} applies global colors and passes"
|
|
2539
|
+
details += f" Normal {level.upper()} at a ratio of "
|
|
2540
|
+
details += f"{contrast_ratio}:1."
|
|
2541
|
+
else:
|
|
2542
|
+
details = f"fail: {filename} does NOT apply global colors"
|
|
2471
2543
|
report.append(details)
|
|
2472
2544
|
if not report:
|
|
2473
2545
|
report.append("fail: no html files to apply color styles to")
|
|
@@ -21,6 +21,7 @@ I will also not support calculating `ch` or `ex` since those are
|
|
|
21
21
|
dependent upon the font-family itself and require information from the
|
|
22
22
|
font-file or some other hidden calculation from the browser.
|
|
23
23
|
"""
|
|
24
|
+
|
|
24
25
|
import re
|
|
25
26
|
from typing import Any
|
|
26
27
|
from typing import Union
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|