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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webcode-tk
3
- Version: 0.7.11
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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "webcode-tk"
3
- version = "0.7.11"
3
+ version = "1.0.4"
4
4
  description = ""
5
5
  authors = ["hundredvisionsguy <cwinikka@gmail.com>"]
6
6
  keywords = ["html", "validation", "css", ]
@@ -18,4 +18,4 @@ Modules exported by this package:
18
18
  - `validator_tools`: sends HTML or CSS code to the W3C Validator to check
19
19
  for errors.
20
20
  """
21
- __version__ = "0.7.11"
21
+ __version__ = "1.0.4"
@@ -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 eacSh style one at a time
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
- "gradiparentent_colors": [],
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][-1]
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
- (contrast, foreground, background, composite_color)
835
+ (
836
+ contrast,
837
+ foreground,
838
+ background,
839
+ composite_color,
840
+ bg_has_alpha,
841
+ )
798
842
  )
799
843
  else:
800
- results.append((contrast, foreground, background, background))
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