webcode-tk 0.7.11__tar.gz → 1.0.4__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-0.7.11 → webcode_tk-1.0.4}/PKG-INFO +2 -1
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/pyproject.toml +1 -1
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/__init__.py +1 -1
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/cascade_tools.py +143 -4
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/color_tools.py +48 -2
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/css_tools.py +254 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/html_tools.py +24 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/validator_tools.py +58 -1
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/LICENSE.rst +0 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/README.rst +0 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/color_keywords.py +0 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/font_tools.py +0 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/sample_report.json +0 -0
- {webcode_tk-0.7.11 → webcode_tk-1.0.4}/webcode_tk/ux_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: webcode-tk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary:
|
|
5
5
|
Home-page: https://github.com/HundredVisionsGuy/webcode-tk
|
|
6
6
|
License: MIT
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
20
|
Requires-Dist: MechanicalSoup (>=1.1.0,<2.0.0)
|
|
20
21
|
Requires-Dist: bs4 (>=0.0.1,<0.0.2)
|
|
21
22
|
Requires-Dist: file-clerk (>=1.0.9,<2.0.0)
|
|
@@ -5,12 +5,13 @@ inheritance and the cascade.
|
|
|
5
5
|
The goal is to use the DOM, inheritance, and the cascade, to determine
|
|
6
6
|
all styles applied to every element on the page.
|
|
7
7
|
|
|
8
|
-
With that, it scans each stylesheet and applies
|
|
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
|
import ctypes
|
|
12
12
|
import re
|
|
13
13
|
from collections import abc
|
|
14
|
+
from typing import Union
|
|
14
15
|
|
|
15
16
|
from file_clerk import clerk
|
|
16
17
|
|
|
@@ -73,12 +74,14 @@ class Element(object):
|
|
|
73
74
|
self.attributes = attributes
|
|
74
75
|
self.background_color = {
|
|
75
76
|
"value": "#ffffff",
|
|
77
|
+
"has_alpha": False,
|
|
78
|
+
"computed_value": "",
|
|
76
79
|
"sheet": "user-agent",
|
|
77
80
|
"selector": "",
|
|
78
81
|
"specificity": "000",
|
|
79
82
|
"applied_by": "context",
|
|
80
83
|
"is_gradient": False,
|
|
81
|
-
"
|
|
84
|
+
"gradient_colors": [],
|
|
82
85
|
}
|
|
83
86
|
self.color = {
|
|
84
87
|
"value": "#000000",
|
|
@@ -129,12 +132,14 @@ class Element(object):
|
|
|
129
132
|
"normal_aaa": True,
|
|
130
133
|
"normal_aa": True,
|
|
131
134
|
"large_aaa": True,
|
|
135
|
+
"large_aa": True,
|
|
132
136
|
}
|
|
133
137
|
self.visited_contrast_data = {
|
|
134
138
|
"ratio": 0.0,
|
|
135
139
|
"normal_aaa": False,
|
|
136
140
|
"normal_aa": False,
|
|
137
141
|
"large_aaa": False,
|
|
142
|
+
"large_aa": False,
|
|
138
143
|
}
|
|
139
144
|
self.__set_link_color()
|
|
140
145
|
self.get_contrast_data("default")
|
|
@@ -166,6 +171,7 @@ class Element(object):
|
|
|
166
171
|
# get relevant specificity
|
|
167
172
|
new_specificity = new_styles.get("specificity")
|
|
168
173
|
col_specificity = self.color.get("specificity")
|
|
174
|
+
|
|
169
175
|
# Check for overrides
|
|
170
176
|
color_has_override = new_specificity >= col_specificity
|
|
171
177
|
|
|
@@ -263,7 +269,12 @@ class Element(object):
|
|
|
263
269
|
col, bg, self.ancestors
|
|
264
270
|
)
|
|
265
271
|
# Set bg to composite color with lowest contrast ratio
|
|
266
|
-
bg = results[0][-
|
|
272
|
+
bg = results[0][-2]
|
|
273
|
+
has_alpha = results[0][-1]
|
|
274
|
+
if has_alpha:
|
|
275
|
+
self.background_color["has_alpha"] = True
|
|
276
|
+
self.background_color["computed_value"] = bg
|
|
277
|
+
|
|
267
278
|
hexc = color.get_hex(col)
|
|
268
279
|
hexbg = color.get_hex(bg)
|
|
269
280
|
self.__build_contrast_report(hexc, hexbg)
|
|
@@ -327,6 +338,9 @@ class Element(object):
|
|
|
327
338
|
self.visited_contrast_data["large_aaa"] = bool(
|
|
328
339
|
link_contrast.get("Large AAA") == "Pass"
|
|
329
340
|
)
|
|
341
|
+
self.visited_contrast_data["large_aa"] = bool(
|
|
342
|
+
link_contrast.get("Large AA") == "Pass"
|
|
343
|
+
)
|
|
330
344
|
|
|
331
345
|
def change_styles(
|
|
332
346
|
self,
|
|
@@ -554,6 +568,7 @@ class Element(object):
|
|
|
554
568
|
directly_targeted = self.directly_targets(selector)
|
|
555
569
|
previously_set = self.background_color.get("applied_by") == "directly"
|
|
556
570
|
bg_gradient = color.is_gradient(val)
|
|
571
|
+
|
|
557
572
|
# If directly, check specificity
|
|
558
573
|
if directly_targeted:
|
|
559
574
|
if previously_set:
|
|
@@ -562,13 +577,49 @@ class Element(object):
|
|
|
562
577
|
colors = []
|
|
563
578
|
if bg_gradient:
|
|
564
579
|
self.background_color["is_gradient"] = True
|
|
580
|
+
c = self.color.get("value")
|
|
581
|
+
details = color.get_color_contrast_with_gradients(
|
|
582
|
+
c, val, self.ancestors
|
|
583
|
+
)
|
|
565
584
|
colors = color.get_gradient_colors(val)
|
|
585
|
+
|
|
586
|
+
# Set gradient colors with highest and lowest contrast
|
|
587
|
+
self.background_color["best_contrast"] = {}
|
|
588
|
+
self.background_color["worst_contrast"] = {}
|
|
589
|
+
|
|
590
|
+
def process_gradient_data(details, bg_dict):
|
|
591
|
+
contrast_ratio = details[0]
|
|
592
|
+
size = self.font_size
|
|
593
|
+
is_bold = self.is_bold
|
|
594
|
+
results = color.get_contrast_results_from_ratio(
|
|
595
|
+
contrast_ratio, size, is_bold
|
|
596
|
+
)
|
|
597
|
+
has_alpha = details[-1]
|
|
598
|
+
if has_alpha:
|
|
599
|
+
bg_dict["has_alpha"] = has_alpha
|
|
600
|
+
self.background_color["has_alpha"] = True
|
|
601
|
+
self.background_color["computed_value"] = details[3]
|
|
602
|
+
bg_dict["computed_value"] = details[-2]
|
|
603
|
+
else:
|
|
604
|
+
bg_dict["value"] = details[2]
|
|
605
|
+
bg_dict["contrast_ratio"] = details[0]
|
|
606
|
+
bg_dict["results"] = results
|
|
607
|
+
|
|
608
|
+
best = details[-1]
|
|
609
|
+
worst = details[0]
|
|
610
|
+
process_gradient_data(
|
|
611
|
+
best, self.background_color["best_contrast"]
|
|
612
|
+
)
|
|
613
|
+
process_gradient_data(
|
|
614
|
+
worst, self.background_color["worst_contrast"]
|
|
615
|
+
)
|
|
566
616
|
self.background_color["gradient_colors"] = colors
|
|
567
617
|
# set color, specificity, and directly applied
|
|
568
618
|
self.background_color["value"] = val
|
|
569
619
|
self.background_color["sheet"] = filename
|
|
570
620
|
self.background_color["specificity"] = new_specificity
|
|
571
621
|
self.background_color["applied_by"] = "directly"
|
|
622
|
+
self.background_color["selector"] = selector
|
|
572
623
|
else:
|
|
573
624
|
if not previously_set:
|
|
574
625
|
self.background_color["value"] = val
|
|
@@ -1441,6 +1492,94 @@ def update_contrast_results(
|
|
|
1441
1492
|
return results
|
|
1442
1493
|
|
|
1443
1494
|
|
|
1495
|
+
def get_color_contrast_details(tree: CSSAppliedTree, rating="AAA") -> list:
|
|
1496
|
+
"""returns a list of all failures or a pass based on rating
|
|
1497
|
+
|
|
1498
|
+
We'll create a recursive inner nested function to traverse the DOM. We will
|
|
1499
|
+
target only the styles that are applied directly so as to not overwhelm the
|
|
1500
|
+
user and allow fixing the error more easily.
|
|
1501
|
+
|
|
1502
|
+
Args:
|
|
1503
|
+
tree: the cascade tree for a single file.
|
|
1504
|
+
rating: the rating of contrast we check for (AAA or AA). The default
|
|
1505
|
+
is AAA.
|
|
1506
|
+
Returns:
|
|
1507
|
+
results"""
|
|
1508
|
+
results = []
|
|
1509
|
+
filename = tree.filename
|
|
1510
|
+
# Loop recursively through the tree
|
|
1511
|
+
elements = tree.children
|
|
1512
|
+
|
|
1513
|
+
def check_element(
|
|
1514
|
+
el: Element, rating: str, filename: str
|
|
1515
|
+
) -> Union[list, None]:
|
|
1516
|
+
children = el.children
|
|
1517
|
+
msg = ""
|
|
1518
|
+
applied_directly = (
|
|
1519
|
+
el.background_color.get("applied_by") == "directly"
|
|
1520
|
+
or el.color.get("applied_by") == "directly"
|
|
1521
|
+
)
|
|
1522
|
+
if applied_directly or el.name == "body":
|
|
1523
|
+
is_gradient = el.background_color.get("is_gradient")
|
|
1524
|
+
if is_gradient:
|
|
1525
|
+
# get worst case
|
|
1526
|
+
worst_case = el.background_color.get("worst_contrast")
|
|
1527
|
+
worst_results = worst_case.get("results")
|
|
1528
|
+
fails = "fail" in worst_results[:5]
|
|
1529
|
+
if fails:
|
|
1530
|
+
selector = el.background_color.get("selector")
|
|
1531
|
+
if el.is_large:
|
|
1532
|
+
size = "Large"
|
|
1533
|
+
else:
|
|
1534
|
+
size = "Normal"
|
|
1535
|
+
msg = f"{el.name} tag (selector: {selector}) fails color "
|
|
1536
|
+
msg += f"contrast at {size} {rating}."
|
|
1537
|
+
results.append(msg)
|
|
1538
|
+
return
|
|
1539
|
+
contrast_data = el.contrast_data
|
|
1540
|
+
if el.is_large:
|
|
1541
|
+
if rating == "AAA":
|
|
1542
|
+
passes = contrast_data.get("large_aaa")
|
|
1543
|
+
else:
|
|
1544
|
+
passes = contrast_data.get("large_aa")
|
|
1545
|
+
else:
|
|
1546
|
+
if rating == "AAA":
|
|
1547
|
+
passes = contrast_data.get("normal_aaa")
|
|
1548
|
+
else:
|
|
1549
|
+
passes = contrast_data.get("normal_aa")
|
|
1550
|
+
# only add results if failure
|
|
1551
|
+
if not passes:
|
|
1552
|
+
if el.is_large:
|
|
1553
|
+
size = "Large"
|
|
1554
|
+
else:
|
|
1555
|
+
size = "Normal"
|
|
1556
|
+
selector = el.background_color.get("selector")
|
|
1557
|
+
if not selector:
|
|
1558
|
+
selector = el.color.get("selector")
|
|
1559
|
+
msg = f"{el.name} tag (selector: {selector}) fails color "
|
|
1560
|
+
msg += f"contrast at {size} {rating}."
|
|
1561
|
+
results.append(msg)
|
|
1562
|
+
if not children:
|
|
1563
|
+
return
|
|
1564
|
+
else:
|
|
1565
|
+
for kid in children:
|
|
1566
|
+
check_element(kid, rating, filename)
|
|
1567
|
+
|
|
1568
|
+
for element in elements:
|
|
1569
|
+
check_element(element, rating, filename)
|
|
1570
|
+
if results:
|
|
1571
|
+
adjusted = []
|
|
1572
|
+
for result in results:
|
|
1573
|
+
adjusted.append("fail: " + result)
|
|
1574
|
+
results = adjusted
|
|
1575
|
+
else:
|
|
1576
|
+
# It's a success! Let's let them know
|
|
1577
|
+
msg = f"success: {filename} passes color contrast for {rating} Normal"
|
|
1578
|
+
msg += " and Large."
|
|
1579
|
+
results.append(msg)
|
|
1580
|
+
return results
|
|
1581
|
+
|
|
1582
|
+
|
|
1444
1583
|
if __name__ == "__main__":
|
|
1445
1584
|
project_path = "tests/test_files/attribute_selector_file"
|
|
1446
1585
|
project_path = "tests/test_files/large_project/"
|
|
@@ -1453,6 +1592,7 @@ if __name__ == "__main__":
|
|
|
1453
1592
|
sheet,
|
|
1454
1593
|
],
|
|
1455
1594
|
)
|
|
1595
|
+
results = get_color_contrast_details(gradient_tree, "AAA")
|
|
1456
1596
|
styles_by_html_files = css.get_styles_by_html_files(project_path)
|
|
1457
1597
|
for file in styles_by_html_files:
|
|
1458
1598
|
filepath = file.get("file")
|
|
@@ -1461,4 +1601,3 @@ if __name__ == "__main__":
|
|
|
1461
1601
|
results = {}
|
|
1462
1602
|
root = css_tree.children[0]
|
|
1463
1603
|
results = get_color_contrast_results(root, results)
|
|
1464
|
-
print(results)
|
|
@@ -78,6 +78,44 @@ def passes_color_contrast(level: str, hex1: str, hex2: str) -> bool:
|
|
|
78
78
|
return passes
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def get_contrast_results_from_ratio(
|
|
82
|
+
ratio: float, size: float, is_bold: bool
|
|
83
|
+
) -> str:
|
|
84
|
+
"""takes contrast, compares to map and returns results as a string
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ratio: the contrast ratio.
|
|
88
|
+
size: calculated font size.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: whether it passes or not with details.
|
|
92
|
+
"""
|
|
93
|
+
# is the text large or not?
|
|
94
|
+
is_large = size >= 24 or is_bold and size >= 16.66
|
|
95
|
+
if is_large:
|
|
96
|
+
aaa_goal = contrast_ratio_map.get("Large AAA")
|
|
97
|
+
passes = ratio >= aaa_goal
|
|
98
|
+
if passes:
|
|
99
|
+
return "pass: Large AAA contrast rating"
|
|
100
|
+
aa_goal = contrast_ratio_map.get("Large AA")
|
|
101
|
+
conditionally_passes = ratio >= aa_goal
|
|
102
|
+
if conditionally_passes:
|
|
103
|
+
return "conditional pass: Large AA contrast rating"
|
|
104
|
+
else:
|
|
105
|
+
return "fail: fails all contrast ratings"
|
|
106
|
+
else:
|
|
107
|
+
aaa_goal = contrast_ratio_map.get("Normal AAA")
|
|
108
|
+
passes = ratio >= aaa_goal
|
|
109
|
+
if passes:
|
|
110
|
+
return "pass: Normal AAA contrast rating"
|
|
111
|
+
aa_goal = contrast_ratio_map.get("Normal AA")
|
|
112
|
+
conditionally_passes = ratio >= aa_goal
|
|
113
|
+
if conditionally_passes:
|
|
114
|
+
return "conditional pass: Normal AA contrast rating"
|
|
115
|
+
else:
|
|
116
|
+
return "fail: fails normal sized text contrast"
|
|
117
|
+
|
|
118
|
+
|
|
81
119
|
def get_color_contrast_report(hex1: str, hex2: str) -> dict:
|
|
82
120
|
"""creates a report on how the two hex colors rate on the color
|
|
83
121
|
contrast chart
|
|
@@ -794,10 +832,18 @@ def get_color_contrast_with_gradients(
|
|
|
794
832
|
# append to results list
|
|
795
833
|
if composite_color:
|
|
796
834
|
results.append(
|
|
797
|
-
(
|
|
835
|
+
(
|
|
836
|
+
contrast,
|
|
837
|
+
foreground,
|
|
838
|
+
background,
|
|
839
|
+
composite_color,
|
|
840
|
+
bg_has_alpha,
|
|
841
|
+
)
|
|
798
842
|
)
|
|
799
843
|
else:
|
|
800
|
-
results.append(
|
|
844
|
+
results.append(
|
|
845
|
+
(contrast, foreground, background, background, False)
|
|
846
|
+
)
|
|
801
847
|
results.sort()
|
|
802
848
|
return results
|
|
803
849
|
|
|
@@ -2222,6 +2222,260 @@ def is_link_selector(selector: str) -> bool:
|
|
|
2222
2222
|
return bool(selector_match)
|
|
2223
2223
|
|
|
2224
2224
|
|
|
2225
|
+
def get_all_project_stylesheets(project_dir: str) -> list:
|
|
2226
|
+
"""returns a list of all styles and stylesheets from a project folder.
|
|
2227
|
+
|
|
2228
|
+
This includes styles from style tags as well as linked stylesheets.
|
|
2229
|
+
|
|
2230
|
+
Args:
|
|
2231
|
+
project_dir: the relative link to the folder with the web docs.
|
|
2232
|
+
|
|
2233
|
+
Returns:
|
|
2234
|
+
all_files_styles: a list of all stylesheets and style tag contents.
|
|
2235
|
+
"""
|
|
2236
|
+
directory = project_dir
|
|
2237
|
+
html_files = clerk.get_all_files_of_type(directory, "html")
|
|
2238
|
+
all_files_styles = []
|
|
2239
|
+
for file in html_files:
|
|
2240
|
+
filename = clerk.get_file_name(file)
|
|
2241
|
+
stylesheets = get_all_stylesheets_by_file(file)
|
|
2242
|
+
all_files_styles.append((filename, stylesheets))
|
|
2243
|
+
return all_files_styles
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def no_style_attributes_allowed_report(project_dir: str) -> list:
|
|
2247
|
+
"""returns a report on whether HTML docs use style attributes or not.
|
|
2248
|
+
|
|
2249
|
+
Only call this report if you do not allow a style attribute in an
|
|
2250
|
+
HTML doc
|
|
2251
|
+
|
|
2252
|
+
Args:
|
|
2253
|
+
project_dir: the relative link to the folder with the web docs.
|
|
2254
|
+
|
|
2255
|
+
Returns:
|
|
2256
|
+
report: a list of all HTML docs and a pass or fail message.
|
|
2257
|
+
"""
|
|
2258
|
+
report = []
|
|
2259
|
+
|
|
2260
|
+
html_files = clerk.get_all_files_of_type(project_dir, "html")
|
|
2261
|
+
for file in html_files:
|
|
2262
|
+
try:
|
|
2263
|
+
has_style_attr = html_tools.has_style_attribute_data(file)
|
|
2264
|
+
except AttributeError:
|
|
2265
|
+
continue
|
|
2266
|
+
if has_style_attr:
|
|
2267
|
+
result = f"fail: {file} uses style attributes"
|
|
2268
|
+
else:
|
|
2269
|
+
result = f"pass: {file} does not use style attributes"
|
|
2270
|
+
report.append(result)
|
|
2271
|
+
return report
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def styles_applied_report(project_dir: str) -> list:
|
|
2275
|
+
"""returns a report of all files in a project folder that apply styles
|
|
2276
|
+
|
|
2277
|
+
This lets us know for each HTML doc if they apply styles (pass) or
|
|
2278
|
+
if they do not apply styles (fail)
|
|
2279
|
+
|
|
2280
|
+
Args:
|
|
2281
|
+
project_dir: a relative path to the project folder.
|
|
2282
|
+
|
|
2283
|
+
Returns:
|
|
2284
|
+
report: a list of HTML docs and whether they pass or fail (pass) means
|
|
2285
|
+
they did apply styles and fail is the opposite.
|
|
2286
|
+
"""
|
|
2287
|
+
report = []
|
|
2288
|
+
html_files = clerk.get_all_files_of_type(project_dir, "html")
|
|
2289
|
+
for file in html_files:
|
|
2290
|
+
styles = get_all_stylesheets_by_file(file)
|
|
2291
|
+
if not styles:
|
|
2292
|
+
results = f"fail: {file} does NOT apply CSS."
|
|
2293
|
+
else:
|
|
2294
|
+
results = f"pass: {file} applies CSS."
|
|
2295
|
+
report.append(results)
|
|
2296
|
+
return report
|
|
2297
|
+
|
|
2298
|
+
|
|
2299
|
+
def fonts_applied_report(project_dir: str, min=1, max=2) -> list:
|
|
2300
|
+
"""returns a report of all files in a project folder that apply font
|
|
2301
|
+
families.
|
|
2302
|
+
|
|
2303
|
+
You can set the minimum and maximum number of fonts applied per page.
|
|
2304
|
+
|
|
2305
|
+
Args:
|
|
2306
|
+
project_dir: the relative path to the project folder we want to check.
|
|
2307
|
+
min: the minimum number of fonts applied per file.
|
|
2308
|
+
max: the maximum number of fonts applied per file.
|
|
2309
|
+
Returns:
|
|
2310
|
+
report: a list of font data results.
|
|
2311
|
+
"""
|
|
2312
|
+
report = []
|
|
2313
|
+
all_file_data = get_styles_by_html_files(project_dir)
|
|
2314
|
+
for file in all_file_data:
|
|
2315
|
+
font_families_applied = []
|
|
2316
|
+
number_of_fonts = 0
|
|
2317
|
+
filename = clerk.get_file_name(file.get("file"))
|
|
2318
|
+
stylesheets = file.get("stylesheets")
|
|
2319
|
+
for sheet in stylesheets:
|
|
2320
|
+
font_details = get_font_families(sheet)
|
|
2321
|
+
number_of_fonts += len(font_details)
|
|
2322
|
+
for item in font_details:
|
|
2323
|
+
results = None
|
|
2324
|
+
selector = item.get("selector")
|
|
2325
|
+
family = item.get("family")
|
|
2326
|
+
if "," in family:
|
|
2327
|
+
first_font = family.split(",")[0].strip()
|
|
2328
|
+
else:
|
|
2329
|
+
first_font = family
|
|
2330
|
+
first_font = first_font.replace("'", "")
|
|
2331
|
+
first_font = first_font.replace('"', "")
|
|
2332
|
+
if first_font not in font_families_applied:
|
|
2333
|
+
font_families_applied.append(first_font)
|
|
2334
|
+
if first_font.lower() == "times new roman":
|
|
2335
|
+
results = f"fail: {filename}: {selector} element was set "
|
|
2336
|
+
results += "to the default font"
|
|
2337
|
+
num_fonts = len(font_families_applied)
|
|
2338
|
+
if num_fonts >= min and num_fonts <= max:
|
|
2339
|
+
results = f"pass: {filename} applied {num_fonts} "
|
|
2340
|
+
if num_fonts == 1:
|
|
2341
|
+
current_font = font_families_applied[0]
|
|
2342
|
+
results += f"font: {current_font}"
|
|
2343
|
+
else:
|
|
2344
|
+
results += "fonts:"
|
|
2345
|
+
for i in range(num_fonts):
|
|
2346
|
+
current_font = font_families_applied[i]
|
|
2347
|
+
if current_font == font_families_applied[-1]:
|
|
2348
|
+
results += f"and {current_font}"
|
|
2349
|
+
elif num_fonts > 2:
|
|
2350
|
+
results += f" {current_font}, "
|
|
2351
|
+
else:
|
|
2352
|
+
results += f" {current_font} "
|
|
2353
|
+
results += "."
|
|
2354
|
+
elif num_fonts < min:
|
|
2355
|
+
results = f"fail: {filename} did not apply {min} fonts, "
|
|
2356
|
+
results += f"instead, it applied {num_fonts} fonts."
|
|
2357
|
+
elif num_fonts > max:
|
|
2358
|
+
results = f"fail: {filename} applied too many fonts; "
|
|
2359
|
+
results += f" it applied {num_fonts} fonts."
|
|
2360
|
+
if results and results not in report:
|
|
2361
|
+
report.append(results)
|
|
2362
|
+
if not report:
|
|
2363
|
+
report.append("fail: no html files to apply font styling to")
|
|
2364
|
+
return report
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
def get_global_color_report(project_dir: str, level="aaa") -> list:
|
|
2368
|
+
"""Returns a report on which files in a project apply global colors
|
|
2369
|
+
|
|
2370
|
+
Args:
|
|
2371
|
+
project_dir: the project folder path.
|
|
2372
|
+
level: whether we are testing for Normal AAA or Normal AA
|
|
2373
|
+
|
|
2374
|
+
Returns:
|
|
2375
|
+
report: a list of files and a pass or fail message for each."""
|
|
2376
|
+
report = []
|
|
2377
|
+
all_file_data = get_all_project_stylesheets(project_dir)
|
|
2378
|
+
for data in all_file_data:
|
|
2379
|
+
filename = data[0]
|
|
2380
|
+
passes = []
|
|
2381
|
+
for sheet in data[1]:
|
|
2382
|
+
rules = sheet.rulesets
|
|
2383
|
+
global_color_data = get_global_color_details(rules)
|
|
2384
|
+
if global_color_data:
|
|
2385
|
+
for item in global_color_data:
|
|
2386
|
+
file, result = get_color_data(filename, item, level)
|
|
2387
|
+
passes.append(f"pass: {file} {result}")
|
|
2388
|
+
if passes:
|
|
2389
|
+
details = ""
|
|
2390
|
+
for detail in passes:
|
|
2391
|
+
details += detail
|
|
2392
|
+
else:
|
|
2393
|
+
details = f"fail: {filename} does NOT apply global colors"
|
|
2394
|
+
report.append(details)
|
|
2395
|
+
if not report:
|
|
2396
|
+
report.append("fail: no html files to apply color styles to")
|
|
2397
|
+
return report
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
def get_color_data(file: str, color_details: dict, level="aaa") -> tuple:
|
|
2401
|
+
"""returns the color contrast data on a color.
|
|
2402
|
+
|
|
2403
|
+
pulls out the selector, background color, text color, contrast
|
|
2404
|
+
ratio, and whether it passes color contrast.
|
|
2405
|
+
|
|
2406
|
+
Args:
|
|
2407
|
+
file: just the name of the file (not path).
|
|
2408
|
+
color_details: tuple of full color & bg color details.
|
|
2409
|
+
level: the level of normal text (AAA or AA).
|
|
2410
|
+
|
|
2411
|
+
Returns:
|
|
2412
|
+
color_data: a tuple with filename and results as a string"""
|
|
2413
|
+
selector = color_details.get("selector")
|
|
2414
|
+
bg_color = color_details.get("background-color")
|
|
2415
|
+
color = color_details.get("color")
|
|
2416
|
+
contrast_ratio = color_details.get("contrast_ratio")
|
|
2417
|
+
if level == "aaa":
|
|
2418
|
+
passes = color_details.get("passes_normal_aaa")
|
|
2419
|
+
else:
|
|
2420
|
+
passes = color_details.get("passes_normal_aa")
|
|
2421
|
+
if passes:
|
|
2422
|
+
results = "passes global colors"
|
|
2423
|
+
else:
|
|
2424
|
+
results = f"{selector} {bg_color} and {color} fail with a contrast"
|
|
2425
|
+
results += f" ratio of {contrast_ratio}."
|
|
2426
|
+
color_data = (file, results)
|
|
2427
|
+
return color_data
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
def get_heading_color_report(project_dir: str) -> list:
|
|
2431
|
+
"""Returns a report on which files in a project apply heading colors
|
|
2432
|
+
|
|
2433
|
+
For now, we just want to have at least a color or background color
|
|
2434
|
+
applied.
|
|
2435
|
+
|
|
2436
|
+
Args:
|
|
2437
|
+
project_dir: the project folder path.
|
|
2438
|
+
|
|
2439
|
+
Returns:
|
|
2440
|
+
report: a list of files and a pass or fail message for each."""
|
|
2441
|
+
report = []
|
|
2442
|
+
header_re = regex_patterns.get("header_selector")
|
|
2443
|
+
if project_dir[-1] != "/":
|
|
2444
|
+
project_dir += "/"
|
|
2445
|
+
all_file_data = get_all_project_stylesheets(project_dir)
|
|
2446
|
+
for file in all_file_data:
|
|
2447
|
+
filename = file[0]
|
|
2448
|
+
filepath = project_dir + filename
|
|
2449
|
+
all_color_rules = get_all_color_rules(filepath)
|
|
2450
|
+
header_selectors = []
|
|
2451
|
+
for key, val in all_color_rules.items():
|
|
2452
|
+
if key == "file":
|
|
2453
|
+
continue
|
|
2454
|
+
is_header_selector = re.findall(header_re, key)
|
|
2455
|
+
if is_header_selector:
|
|
2456
|
+
# we have a header selector
|
|
2457
|
+
# we only need to check color
|
|
2458
|
+
color_data = val
|
|
2459
|
+
color_value = color_data.get("color")
|
|
2460
|
+
bg_value = color_data.get("background-color")
|
|
2461
|
+
if color_value:
|
|
2462
|
+
if bg_value:
|
|
2463
|
+
header_selectors.append(
|
|
2464
|
+
(filename, color_value, bg_value)
|
|
2465
|
+
)
|
|
2466
|
+
else:
|
|
2467
|
+
header_selectors.append((filename, color_value, None))
|
|
2468
|
+
if bg_value:
|
|
2469
|
+
header_selectors.append((filename, None, bg_value))
|
|
2470
|
+
if header_selectors:
|
|
2471
|
+
report.append(f"pass: {filename} applies colors to headers")
|
|
2472
|
+
else:
|
|
2473
|
+
report.append(f"fail: {filename} does NOT apply colors to headers")
|
|
2474
|
+
if not report:
|
|
2475
|
+
report.append("fail: no html files to apply header colors to")
|
|
2476
|
+
return report
|
|
2477
|
+
|
|
2478
|
+
|
|
2225
2479
|
if __name__ == "__main__":
|
|
2226
2480
|
insane_gradient = """
|
|
2227
2481
|
-moz-radial-gradient(0% 200%, ellipse cover,
|
|
@@ -410,6 +410,30 @@ def add_if_not_in(my_list: list, item: str) -> None:
|
|
|
410
410
|
my_list.append(item)
|
|
411
411
|
|
|
412
412
|
|
|
413
|
+
def get_number_of_elements_per_file(
|
|
414
|
+
project_dir: str, element_data: list
|
|
415
|
+
) -> list:
|
|
416
|
+
"""returns a list of number of elements per file in a project folder.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
project_dir: the folder we want to check
|
|
420
|
+
element_data: a list of tuples of element (str) and required number
|
|
421
|
+
(int)
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
elements_per_file: a list of tuples that includes the file, the
|
|
425
|
+
element, and the number of occurrences.
|
|
426
|
+
"""
|
|
427
|
+
elements_per_file = []
|
|
428
|
+
all_html_files = get_all_html_files(project_dir)
|
|
429
|
+
for file in all_html_files:
|
|
430
|
+
for i in range(len(element_data)):
|
|
431
|
+
# Get requirements for exact number
|
|
432
|
+
element, number = element_data[i]
|
|
433
|
+
elements_per_file.append((file, element, number))
|
|
434
|
+
return elements_per_file
|
|
435
|
+
|
|
436
|
+
|
|
413
437
|
if __name__ == "__main__":
|
|
414
438
|
file_with_inline_styles = "tests/test_files/sample_with_inline_styles.html"
|
|
415
439
|
markup = clerk.file_to_string(file_with_inline_styles)
|
|
@@ -7,6 +7,8 @@ import requests
|
|
|
7
7
|
from bs4 import BeautifulSoup
|
|
8
8
|
from file_clerk import clerk
|
|
9
9
|
|
|
10
|
+
from webcode_tk import html_tools as html
|
|
11
|
+
|
|
10
12
|
w3cURL = "https://validator.w3.org/nu/?out=json"
|
|
11
13
|
|
|
12
14
|
# Instantiate a stateful browser
|
|
@@ -89,7 +91,7 @@ def get_markup_validity(file_path: str) -> list:
|
|
|
89
91
|
errors: a list of dictionary types (converted from the JSON
|
|
90
92
|
response from the validator."""
|
|
91
93
|
errors = []
|
|
92
|
-
payload = open(file_path)
|
|
94
|
+
# payload = open(file_path)
|
|
93
95
|
with open(file_path, "rb") as payload:
|
|
94
96
|
headers = {
|
|
95
97
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -253,6 +255,61 @@ def validate_css(css_code: str) -> bs4.ResultSet:
|
|
|
253
255
|
return results
|
|
254
256
|
|
|
255
257
|
|
|
258
|
+
def get_project_validation(project_dir: str, type="html") -> list:
|
|
259
|
+
"""returns a report on HTML or CSS validation per HTML file.
|
|
260
|
+
|
|
261
|
+
You choose the project folder and the type (html or css), and it
|
|
262
|
+
will return a list of per-files errors
|
|
263
|
+
"""
|
|
264
|
+
report = []
|
|
265
|
+
passing_files = []
|
|
266
|
+
all_files = clerk.get_all_project_files(project_dir)
|
|
267
|
+
for file in all_files:
|
|
268
|
+
errors = []
|
|
269
|
+
file_type = clerk.get_file_type(file)
|
|
270
|
+
filename = clerk.get_file_name(file)
|
|
271
|
+
if type == "html" and file_type == "html":
|
|
272
|
+
errors = get_markup_validity(file)
|
|
273
|
+
if errors:
|
|
274
|
+
report.append(
|
|
275
|
+
f"fail: {filename} has {len(errors)} validation errors."
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
passing_files.append(filename)
|
|
279
|
+
else:
|
|
280
|
+
if file_type == "html" and type == "css":
|
|
281
|
+
style_tag = html.get_elements("style", file)
|
|
282
|
+
if style_tag:
|
|
283
|
+
code = html.get_element_content(style_tag)
|
|
284
|
+
result = validate_css(code)
|
|
285
|
+
errors_list = get_css_errors_list(result)
|
|
286
|
+
if errors_list:
|
|
287
|
+
errors += errors_list
|
|
288
|
+
else:
|
|
289
|
+
passing_files.append(filename)
|
|
290
|
+
if file_type == "css":
|
|
291
|
+
code = clerk.file_to_string(file)
|
|
292
|
+
result = validate_css(code)
|
|
293
|
+
errors_list = get_css_errors_list(result)
|
|
294
|
+
if errors_list:
|
|
295
|
+
errors += errors_list
|
|
296
|
+
else:
|
|
297
|
+
passing_files.append(filename)
|
|
298
|
+
if errors:
|
|
299
|
+
report.append(
|
|
300
|
+
f"fail: {filename} has {len(errors)} css errors."
|
|
301
|
+
)
|
|
302
|
+
if not report:
|
|
303
|
+
if passing_files:
|
|
304
|
+
for passing_file in passing_files:
|
|
305
|
+
msg = f"pass: {passing_file} passes {type.upper()} validation"
|
|
306
|
+
report.append(msg)
|
|
307
|
+
if not passing_files:
|
|
308
|
+
report.append("fail: no files present to validate")
|
|
309
|
+
# TODO - make sure this covers all scenarios
|
|
310
|
+
return report
|
|
311
|
+
|
|
312
|
+
|
|
256
313
|
if __name__ == "__main__":
|
|
257
314
|
path = "tests/test_files/sample_with_errors.html"
|
|
258
315
|
report = get_markup_validity(path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|