webcode-tk 1.2.12__tar.gz → 1.2.18__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.12 → webcode_tk-1.2.18}/PKG-INFO +2 -1
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/pyproject.toml +2 -1
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/__init__.py +1 -1
- webcode_tk-1.2.18/webcode_tk/_style_inheritance.py +371 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/animation_tools.py +1 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/cascade_tools.py +1 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/color_keywords.py +1 -1
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/color_tools.py +1 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/contrast_tools.py +58 -361
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/css_tools.py +4 -1
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/font_tools.py +101 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/html_tools.py +1 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/ux_tools.py +1 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/LICENSE.rst +0 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/README.rst +0 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/sample_report.json +0 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/utils.py +0 -0
- {webcode_tk-1.2.12 → webcode_tk-1.2.18}/webcode_tk/validator_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webcode-tk
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.18
|
|
4
4
|
Summary: A set of tools for inspecting HTML and CSS code
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE.rst
|
|
@@ -23,6 +23,7 @@ Requires-Dist: mechanicalsoup (>=1.4.0,<2.0.0)
|
|
|
23
23
|
Requires-Dist: nltk (>=3.8.1,<4.0.0)
|
|
24
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.18"
|
|
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,6 +28,7 @@ lxml = "^6.0.0"
|
|
|
28
28
|
mechanicalsoup = "^1.4.0"
|
|
29
29
|
pytest-mock = "^3.15.1"
|
|
30
30
|
pre-commit = "^4.5.0"
|
|
31
|
+
pytest = "^9.0.3"
|
|
31
32
|
|
|
32
33
|
[tool.poetry.group.dev.dependencies]
|
|
33
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
|
|
@@ -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
|
|
@@ -871,12 +878,16 @@ def apply_rule_to_element(
|
|
|
871
878
|
"specificity": rule_specificity,
|
|
872
879
|
"source": "rule",
|
|
873
880
|
"selector": selector,
|
|
874
|
-
"css_file":
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
+
),
|
|
880
891
|
}
|
|
881
892
|
|
|
882
893
|
# Check if we should apply this font-size rule
|
|
@@ -911,12 +922,16 @@ def apply_rule_to_element(
|
|
|
911
922
|
"specificity": rule_specificity,
|
|
912
923
|
"source": "rule",
|
|
913
924
|
"selector": selector,
|
|
914
|
-
"css_file":
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
+
),
|
|
920
935
|
"contrast_analysis": "indeterminate",
|
|
921
936
|
"reason": "background_image_blocks_color_analysis",
|
|
922
937
|
"original_background": property_value,
|
|
@@ -933,16 +948,22 @@ def apply_rule_to_element(
|
|
|
933
948
|
"specificity": rule_specificity,
|
|
934
949
|
"source": "rule",
|
|
935
950
|
"selector": selector,
|
|
936
|
-
"css_file":
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
+
),
|
|
942
961
|
"contrast_analysis": "determinable",
|
|
943
|
-
"original_background":
|
|
944
|
-
|
|
945
|
-
|
|
962
|
+
"original_background": (
|
|
963
|
+
property_value
|
|
964
|
+
if property_value != effective_color
|
|
965
|
+
else None
|
|
966
|
+
),
|
|
946
967
|
}
|
|
947
968
|
|
|
948
969
|
# Apply specificity logic and set the property
|
|
@@ -1000,12 +1021,16 @@ def apply_rule_to_element(
|
|
|
1000
1021
|
"specificity": rule_specificity,
|
|
1001
1022
|
"source": "rule",
|
|
1002
1023
|
"selector": selector,
|
|
1003
|
-
"css_file":
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
+
),
|
|
1009
1034
|
}
|
|
1010
1035
|
|
|
1011
1036
|
return
|
|
@@ -1127,149 +1152,6 @@ def apply_inheritance(soup: BeautifulSoup, computed_styles: dict) -> None:
|
|
|
1127
1152
|
apply_visual_background_inheritance(computed_styles)
|
|
1128
1153
|
|
|
1129
1154
|
|
|
1130
|
-
def apply_css_inheritance(computed_styles: dict) -> None:
|
|
1131
|
-
"""
|
|
1132
|
-
Apply traditional CSS inheritance for inheritable properties.
|
|
1133
|
-
Only inherit from the closest ancestor with an explicit rule.
|
|
1134
|
-
"""
|
|
1135
|
-
inheritable_props = {"color", "font-size", "font-weight"}
|
|
1136
|
-
|
|
1137
|
-
for child_element in computed_styles:
|
|
1138
|
-
child_styles = computed_styles[child_element]
|
|
1139
|
-
|
|
1140
|
-
# find closest ancestor with explicit value
|
|
1141
|
-
for prop in inheritable_props:
|
|
1142
|
-
child_prop = child_styles.get(prop)
|
|
1143
|
-
|
|
1144
|
-
# Skip if child has explicit CSS rule (not default)
|
|
1145
|
-
if child_prop and child_prop.get("source") not in [
|
|
1146
|
-
"default",
|
|
1147
|
-
None,
|
|
1148
|
-
]:
|
|
1149
|
-
continue
|
|
1150
|
-
|
|
1151
|
-
# Walk up tree to find first ancestor with explicit value
|
|
1152
|
-
current = child_element.parent
|
|
1153
|
-
inherited_value = None
|
|
1154
|
-
|
|
1155
|
-
while current and not inherited_value:
|
|
1156
|
-
if current in computed_styles:
|
|
1157
|
-
parent_styles = computed_styles[current]
|
|
1158
|
-
parent_prop = parent_styles.get(prop)
|
|
1159
|
-
|
|
1160
|
-
# Found a parent with this property
|
|
1161
|
-
if parent_prop:
|
|
1162
|
-
# Only inherit if parent has explicit rule
|
|
1163
|
-
if parent_prop.get("source") == "rule":
|
|
1164
|
-
inherited_value = parent_prop
|
|
1165
|
-
inherited_from = current
|
|
1166
|
-
break # ← Stop at first explicit ancestor
|
|
1167
|
-
|
|
1168
|
-
current = current.parent
|
|
1169
|
-
|
|
1170
|
-
# Apply inherited value if found
|
|
1171
|
-
if inherited_value:
|
|
1172
|
-
child_styles[prop] = {
|
|
1173
|
-
"value": inherited_value["value"],
|
|
1174
|
-
"specificity": inherited_value["specificity"],
|
|
1175
|
-
"source": "inheritance",
|
|
1176
|
-
"selector": inherited_value.get("selector", "inherited"),
|
|
1177
|
-
"css_file": inherited_value.get("css_file"),
|
|
1178
|
-
"css_source_type": inherited_value.get("css_source_type"),
|
|
1179
|
-
"inherited_from": inherited_from,
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
def apply_visual_background_inheritance(computed_styles: dict) -> None:
|
|
1184
|
-
"""
|
|
1185
|
-
Apply visual background colors to elements without explicit backgrounds.
|
|
1186
|
-
|
|
1187
|
-
Args:
|
|
1188
|
-
computed_styles: a dictionary of elements with computed styles.
|
|
1189
|
-
Returns:
|
|
1190
|
-
None
|
|
1191
|
-
"""
|
|
1192
|
-
|
|
1193
|
-
for element in computed_styles:
|
|
1194
|
-
element_styles = computed_styles[element]
|
|
1195
|
-
|
|
1196
|
-
# Check for BOTH background-color AND background
|
|
1197
|
-
current_bg = element_styles.get("background-color")
|
|
1198
|
-
has_background_shorthand = "background" in element_styles
|
|
1199
|
-
|
|
1200
|
-
# If element has background shorthand, process it for images
|
|
1201
|
-
if has_background_shorthand:
|
|
1202
|
-
bg_value = element_styles["background"]["value"]
|
|
1203
|
-
|
|
1204
|
-
# Check if background contains a raster image
|
|
1205
|
-
if contains_raster_image(bg_value):
|
|
1206
|
-
# Mark as contrast-indeterminate - cannot analyze
|
|
1207
|
-
element_styles["background-color"] = {
|
|
1208
|
-
"value": None,
|
|
1209
|
-
"specificity": "000",
|
|
1210
|
-
"contrast_analysis": "indeterminate",
|
|
1211
|
-
"reason": "background_image_blocks_color_analysis",
|
|
1212
|
-
"original_background": bg_value,
|
|
1213
|
-
"visual_inheritance": False,
|
|
1214
|
-
}
|
|
1215
|
-
continue
|
|
1216
|
-
else:
|
|
1217
|
-
# Has usable background shorthand - skip inheritance
|
|
1218
|
-
continue
|
|
1219
|
-
if not current_bg:
|
|
1220
|
-
continue
|
|
1221
|
-
|
|
1222
|
-
bg_source = current_bg.get("source")
|
|
1223
|
-
bg_value = current_bg.get("value")
|
|
1224
|
-
|
|
1225
|
-
# Skip elements with explicit CSS rules
|
|
1226
|
-
if bg_source == "rule":
|
|
1227
|
-
continue
|
|
1228
|
-
|
|
1229
|
-
# Only update elements that still have the default white background
|
|
1230
|
-
# This prevents overwriting elements with existing proper backgrounds
|
|
1231
|
-
if bg_source != "default":
|
|
1232
|
-
continue
|
|
1233
|
-
|
|
1234
|
-
# Element needs background inheritance (lines 1120-1141)
|
|
1235
|
-
ancestor_bg = find_ancestor_background(element, computed_styles)
|
|
1236
|
-
|
|
1237
|
-
if not ancestor_bg or ancestor_bg.get("source") == "default":
|
|
1238
|
-
# No explicit background to inherit - keep browser default
|
|
1239
|
-
continue
|
|
1240
|
-
|
|
1241
|
-
if ancestor_bg.get("contrast_analysis") == "indeterminate":
|
|
1242
|
-
# Ancestor is indeterminate - propagate that status
|
|
1243
|
-
element_styles["background-color"] = {
|
|
1244
|
-
"value": None,
|
|
1245
|
-
"specificity": "000",
|
|
1246
|
-
"contrast_analysis": "indeterminate",
|
|
1247
|
-
"reason": ancestor_bg["reason"],
|
|
1248
|
-
"inherited_from": ancestor_bg["source_element"],
|
|
1249
|
-
"source": "visual_inheritance",
|
|
1250
|
-
"original_background": ancestor_bg.get("original_background"),
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
else:
|
|
1254
|
-
# Normal inheritance - extract usable color
|
|
1255
|
-
contrast_color = extract_contrast_color(ancestor_bg["value"])
|
|
1256
|
-
effective_color = (
|
|
1257
|
-
contrast_color if contrast_color else ancestor_bg["value"]
|
|
1258
|
-
)
|
|
1259
|
-
element_styles["background-color"] = {
|
|
1260
|
-
"value": effective_color,
|
|
1261
|
-
"specificity": "000",
|
|
1262
|
-
"source": "visual_inheritance",
|
|
1263
|
-
"inherited_from": ancestor_bg["source_element"],
|
|
1264
|
-
"contrast_analysis": "determinable",
|
|
1265
|
-
"original_background": (
|
|
1266
|
-
ancestor_bg["value"]
|
|
1267
|
-
if ancestor_bg["value"] != DEFAULT_GLOBAL_BACKGROUND
|
|
1268
|
-
else None
|
|
1269
|
-
),
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
1155
|
def has_usable_background_color(background_prop: dict) -> bool:
|
|
1274
1156
|
"""
|
|
1275
1157
|
Check if background property contains usable color for contrast.
|
|
@@ -1299,193 +1181,6 @@ def has_usable_background_color(background_prop: dict) -> bool:
|
|
|
1299
1181
|
)
|
|
1300
1182
|
|
|
1301
1183
|
|
|
1302
|
-
def contains_raster_image(background_value: str) -> bool:
|
|
1303
|
-
"""Check if background contains a raster image that blocks color analysis.
|
|
1304
|
-
|
|
1305
|
-
Args:
|
|
1306
|
-
background_value: the css value applied to the background property.
|
|
1307
|
-
|
|
1308
|
-
Returns:
|
|
1309
|
-
bool: whether the background value includes a raster image or not.
|
|
1310
|
-
"""
|
|
1311
|
-
# Add None check
|
|
1312
|
-
if background_value is None:
|
|
1313
|
-
return False
|
|
1314
|
-
|
|
1315
|
-
return bool(
|
|
1316
|
-
re.search(
|
|
1317
|
-
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
1318
|
-
)
|
|
1319
|
-
)
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
def extract_contrast_color(background_value: str) -> str:
|
|
1323
|
-
"""
|
|
1324
|
-
Extract usable color for contrast analysis, or None.
|
|
1325
|
-
|
|
1326
|
-
Args:
|
|
1327
|
-
background_value (str): CSS background property value.
|
|
1328
|
-
|
|
1329
|
-
Returns:
|
|
1330
|
-
str: Color value suitable for contrast analysis, or None if no
|
|
1331
|
-
usable color found.
|
|
1332
|
-
"""
|
|
1333
|
-
# Add None check at the beginning
|
|
1334
|
-
if background_value is None:
|
|
1335
|
-
return None
|
|
1336
|
-
|
|
1337
|
-
# Remove raster images - they block color visibility
|
|
1338
|
-
if re.search(
|
|
1339
|
-
r"url\([^)]*\.(jpg|jpeg|png|gif|webp|bmp)", background_value, re.I
|
|
1340
|
-
):
|
|
1341
|
-
return None
|
|
1342
|
-
|
|
1343
|
-
# Handle gradients - extract representative color
|
|
1344
|
-
if "gradient" in background_value.lower():
|
|
1345
|
-
return extract_gradient_contrast_color(background_value)
|
|
1346
|
-
|
|
1347
|
-
# Handle solid colors
|
|
1348
|
-
re_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|"
|
|
1349
|
-
re_pattern += r"hsla\([^)]+\)|[a-z]+)"
|
|
1350
|
-
color_match = re.search(
|
|
1351
|
-
re_pattern,
|
|
1352
|
-
background_value,
|
|
1353
|
-
re.I,
|
|
1354
|
-
)
|
|
1355
|
-
return color_match.group(1) if color_match else None
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
def extract_gradient_contrast_color(background_value: str) -> str:
|
|
1359
|
-
"""
|
|
1360
|
-
Extract representative color from gradient for contrast analysis.
|
|
1361
|
-
|
|
1362
|
-
Args:
|
|
1363
|
-
background_value (str): CSS gradient value (e.g.,
|
|
1364
|
-
'linear-gradient(red, blue)').
|
|
1365
|
-
|
|
1366
|
-
Returns:
|
|
1367
|
-
str: Representative color for contrast analysis, or None if no
|
|
1368
|
-
color found.
|
|
1369
|
-
"""
|
|
1370
|
-
# Add None check
|
|
1371
|
-
if background_value is None:
|
|
1372
|
-
return None
|
|
1373
|
-
|
|
1374
|
-
# Strategy: Use the final color in gradient (most visible for reading)
|
|
1375
|
-
# This handles: linear-gradient(red, blue) -> blue
|
|
1376
|
-
# radial-gradient(center, red, blue) -> blue
|
|
1377
|
-
|
|
1378
|
-
# Find all colors in gradient
|
|
1379
|
-
color_pattern = r"(#[a-f0-9]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|"
|
|
1380
|
-
color_pattern += r"hsl\([^)]+\)|hsla\([^)]+\)|[a-z]+)"
|
|
1381
|
-
colors = re.findall(color_pattern, background_value, re.I)
|
|
1382
|
-
|
|
1383
|
-
if colors:
|
|
1384
|
-
# Use last color (end of gradient)
|
|
1385
|
-
return colors[-1]
|
|
1386
|
-
|
|
1387
|
-
return None
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
def find_ancestor_background(element: Tag, computed_styles: dict) -> dict:
|
|
1391
|
-
"""
|
|
1392
|
-
Walk up DOM tree to find first ancestor with background.
|
|
1393
|
-
|
|
1394
|
-
Args:
|
|
1395
|
-
element (Tag): BeautifulSoup Tag object to start searching from.
|
|
1396
|
-
computed_styles (dict): Dictionary mapping elements to their
|
|
1397
|
-
computed styles.
|
|
1398
|
-
|
|
1399
|
-
Returns:
|
|
1400
|
-
dict: Dictionary containing 'value' (background color) and
|
|
1401
|
-
'source_element' (ancestor element or None).
|
|
1402
|
-
"""
|
|
1403
|
-
current = element.parent
|
|
1404
|
-
|
|
1405
|
-
while current:
|
|
1406
|
-
if current in computed_styles:
|
|
1407
|
-
current_styles = computed_styles[current]
|
|
1408
|
-
|
|
1409
|
-
# Flag to track if we should skip this ancestor
|
|
1410
|
-
skip_ancestor = False
|
|
1411
|
-
|
|
1412
|
-
# Check for background-color or background
|
|
1413
|
-
for bg_prop in ["background-color", "background"]:
|
|
1414
|
-
if bg_prop in current_styles:
|
|
1415
|
-
bg_source = current_styles[bg_prop].get("source")
|
|
1416
|
-
|
|
1417
|
-
# Skip elements that only have visual inheritance
|
|
1418
|
-
if bg_source == "visual_inheritance":
|
|
1419
|
-
skip_ancestor = True
|
|
1420
|
-
break # ✅ Break out of property loop
|
|
1421
|
-
|
|
1422
|
-
# Accept both "rule" and "default" sources
|
|
1423
|
-
if bg_source in ["rule", "default"]:
|
|
1424
|
-
bg_value = current_styles[bg_prop]["value"]
|
|
1425
|
-
if bg_source == "default":
|
|
1426
|
-
# Skip ancestors with only default backgrounds
|
|
1427
|
-
skip_ancestor = True
|
|
1428
|
-
break
|
|
1429
|
-
|
|
1430
|
-
# Does this ancestor already is indeterminate status
|
|
1431
|
-
if (
|
|
1432
|
-
current_styles[bg_prop].get("contrast_analysis")
|
|
1433
|
-
== "indeterminate"
|
|
1434
|
-
):
|
|
1435
|
-
return {
|
|
1436
|
-
"value": None,
|
|
1437
|
-
"source_element": current,
|
|
1438
|
-
"contrast_analysis": "indeterminate",
|
|
1439
|
-
"reason": "ancestor_has_background_image",
|
|
1440
|
-
"original_background": current_styles[
|
|
1441
|
-
bg_prop
|
|
1442
|
-
].get("original_background", bg_value),
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
# Skip None values and continue searching
|
|
1446
|
-
if bg_value is None:
|
|
1447
|
-
continue # Continue to next property
|
|
1448
|
-
|
|
1449
|
-
# Check if this background contains a raster image
|
|
1450
|
-
if contains_raster_image(bg_value):
|
|
1451
|
-
return {
|
|
1452
|
-
"value": None,
|
|
1453
|
-
"source_element": current,
|
|
1454
|
-
"contrast_analysis": "indeterminate",
|
|
1455
|
-
"reason": "ancestor_has_background_image",
|
|
1456
|
-
"original_background": bg_value,
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
# Found explicit background
|
|
1460
|
-
bg_items = current_styles[bg_prop].items()
|
|
1461
|
-
return {
|
|
1462
|
-
"value": bg_value,
|
|
1463
|
-
"source_element": current,
|
|
1464
|
-
"contrast_analysis": "determinable",
|
|
1465
|
-
# Copy metadata from ancestor
|
|
1466
|
-
**{
|
|
1467
|
-
metadata_key: metadata_value
|
|
1468
|
-
for metadata_key, metadata_value in bg_items
|
|
1469
|
-
if metadata_key not in ["value"]
|
|
1470
|
-
},
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
# If we flagged this ancestor to skip, move to next ancestor
|
|
1474
|
-
if skip_ancestor:
|
|
1475
|
-
current = current.parent
|
|
1476
|
-
continue # ✅ Now this continues to next ancestor
|
|
1477
|
-
|
|
1478
|
-
current = current.parent
|
|
1479
|
-
|
|
1480
|
-
# No ancestor background found, use default
|
|
1481
|
-
return {
|
|
1482
|
-
"value": DEFAULT_GLOBAL_BACKGROUND,
|
|
1483
|
-
"source_element": None,
|
|
1484
|
-
"contrast_analysis": "determinable",
|
|
1485
|
-
"source": "default",
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
1184
|
def is_inheritable_property(property_name: str) -> bool:
|
|
1490
1185
|
"""
|
|
1491
1186
|
Determines if a CSS property is inheritable by default.
|
|
@@ -1650,9 +1345,11 @@ def extract_property_source(property_data: dict) -> dict:
|
|
|
1650
1345
|
"source_type": "visual_inheritance",
|
|
1651
1346
|
"css_file": "visual_cascade",
|
|
1652
1347
|
"selector": "ancestor_background",
|
|
1653
|
-
"inherited_from":
|
|
1654
|
-
|
|
1655
|
-
|
|
1348
|
+
"inherited_from": (
|
|
1349
|
+
getattr(inherited_element, "name", None)
|
|
1350
|
+
if inherited_element
|
|
1351
|
+
else None
|
|
1352
|
+
),
|
|
1656
1353
|
}
|
|
1657
1354
|
elif source_type == "rule":
|
|
1658
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
|
|
@@ -1361,6 +1362,8 @@ def get_font_families(sheet: Stylesheet) -> list:
|
|
|
1361
1362
|
# always take the last family as it would be an override in CSS
|
|
1362
1363
|
family = families[-1]
|
|
1363
1364
|
font_families.append({"selector": selector, "family": family})
|
|
1365
|
+
else:
|
|
1366
|
+
print()
|
|
1364
1367
|
return font_families
|
|
1365
1368
|
|
|
1366
1369
|
|
|
@@ -21,11 +21,15 @@ 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
|
|
28
|
+
from file_clerk import clerk
|
|
27
29
|
|
|
28
30
|
from webcode_tk import css_tools
|
|
31
|
+
from webcode_tk import html_tools as html
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
absolute_keyword_regex = (
|
|
31
35
|
r"\b(?:xx-small|x-small|small(?!-caps)|medium|large|x-large|"
|
|
@@ -356,6 +360,103 @@ def is_valid_shorthand(value: str) -> bool:
|
|
|
356
360
|
return is_valid
|
|
357
361
|
|
|
358
362
|
|
|
363
|
+
def get_google_font_report(project_dir: str, min=1, max=2) -> str:
|
|
364
|
+
"""determines if the page is using the min number of google fonts
|
|
365
|
+
|
|
366
|
+
We're looking for three things in each web page:
|
|
367
|
+
1) a link in the head to the googlefonts API,
|
|
368
|
+
2) each of those fonts being applied, and
|
|
369
|
+
3) at least the minimum number of unique Google fonts and no more than the
|
|
370
|
+
max #.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
project_dir: the folder where the files reside
|
|
374
|
+
min: minimum number of Google font families in use
|
|
375
|
+
max: maximum number of Google font families in use
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
report: a pass/fail report with accompanying data
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
# Loop through all HTML docs and their styles
|
|
382
|
+
report = []
|
|
383
|
+
styles = css_tools.get_styles_by_html_files(project_dir)
|
|
384
|
+
for data in styles:
|
|
385
|
+
|
|
386
|
+
# Step 1: get all linked Google fonts
|
|
387
|
+
file = data.get("file")
|
|
388
|
+
filename = clerk.get_file_name(file)
|
|
389
|
+
links = html.get_elements("link", file)
|
|
390
|
+
google_fonts_linked = []
|
|
391
|
+
|
|
392
|
+
for link in links:
|
|
393
|
+
href = link.attrs.get("href")
|
|
394
|
+
if (
|
|
395
|
+
"fonts.googleapis.com" in href
|
|
396
|
+
and href != "https://fonts.googleapis.com"
|
|
397
|
+
):
|
|
398
|
+
fonts = get_google_font_data(href)
|
|
399
|
+
if isinstance(fonts, list):
|
|
400
|
+
google_fonts_linked += fonts
|
|
401
|
+
else:
|
|
402
|
+
google_fonts_linked.append(fonts)
|
|
403
|
+
|
|
404
|
+
# Check to see the linked fonts match required number.
|
|
405
|
+
num_fonts = len(google_fonts_linked)
|
|
406
|
+
if num_fonts < min:
|
|
407
|
+
data = f"fail: {filename} does not link to min number of Google "
|
|
408
|
+
data += "fonts."
|
|
409
|
+
report.append(data)
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
# if we have any google fonts, are they being used?
|
|
413
|
+
stylesheets = data.get("stylesheets")
|
|
414
|
+
fonts_used = []
|
|
415
|
+
for sheet in stylesheets:
|
|
416
|
+
families = []
|
|
417
|
+
font_data = css_tools.get_font_families(sheet)
|
|
418
|
+
if font_data:
|
|
419
|
+
families = extract_families(font_data)
|
|
420
|
+
if families:
|
|
421
|
+
for family in google_fonts_linked:
|
|
422
|
+
if family in families:
|
|
423
|
+
if family not in fonts_used:
|
|
424
|
+
fonts_used.append(family)
|
|
425
|
+
|
|
426
|
+
num_used = len(fonts_used)
|
|
427
|
+
if num_used >= min and num_used <= max:
|
|
428
|
+
data = f"pass: {filename} uses the correct number of Google fonts"
|
|
429
|
+
report.append(data)
|
|
430
|
+
else:
|
|
431
|
+
data = f"fail: {filename} with {num_used} font families does not "
|
|
432
|
+
data += "have enough applied Google fonts"
|
|
433
|
+
report.append(data)
|
|
434
|
+
return report
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def extract_families(family_data: list) -> list:
|
|
438
|
+
families = []
|
|
439
|
+
for datum in family_data:
|
|
440
|
+
family_value = datum.get("family")
|
|
441
|
+
family = family_value.split(",")[0]
|
|
442
|
+
family = family.strip()
|
|
443
|
+
family = family.replace('"', "")
|
|
444
|
+
family = family.replace("'", "")
|
|
445
|
+
families.append(family)
|
|
446
|
+
return families
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def get_google_font_data(href: str) -> list:
|
|
450
|
+
font_families = []
|
|
451
|
+
font_data = href.split("family=")
|
|
452
|
+
font_data.pop(0)
|
|
453
|
+
for item in font_data:
|
|
454
|
+
family = item.split(":")[0]
|
|
455
|
+
family = family.replace("+", " ")
|
|
456
|
+
font_families.append(family)
|
|
457
|
+
return font_families
|
|
458
|
+
|
|
459
|
+
|
|
359
460
|
if __name__ == "__main__":
|
|
360
461
|
sample_styles = """
|
|
361
462
|
/* <absolute-size> values */
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|