webcode-tk 1.1.5__tar.gz → 1.1.7__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.1.5 → webcode_tk-1.1.7}/PKG-INFO +2 -2
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/pyproject.toml +1 -1
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/__init__.py +1 -1
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/cascade_tools.py +15 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/css_tools.py +224 -60
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/LICENSE.rst +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/README.rst +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/color_keywords.py +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/color_tools.py +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/font_tools.py +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/html_tools.py +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/sample_report.json +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/ux_tools.py +0 -0
- {webcode_tk-1.1.5 → webcode_tk-1.1.7}/webcode_tk/validator_tools.py +0 -0
|
@@ -315,6 +315,16 @@ class Element(object):
|
|
|
315
315
|
type: the type of color contrast.
|
|
316
316
|
"""
|
|
317
317
|
col = self.color.get("value")
|
|
318
|
+
if not col:
|
|
319
|
+
for i in range(len(self.ancestors) - 1, -1, -1):
|
|
320
|
+
anc = self.ancestors[i]
|
|
321
|
+
if len(anc) > 2:
|
|
322
|
+
color_details = anc[4]
|
|
323
|
+
col = color_details.split(":")[1]
|
|
324
|
+
col = col.strip()
|
|
325
|
+
col = col.split()[0]
|
|
326
|
+
if color.is_color_value(col):
|
|
327
|
+
break
|
|
318
328
|
bg = self.background_color.get("value")
|
|
319
329
|
if color.is_gradient(bg):
|
|
320
330
|
results = color.get_color_contrast_with_gradients(
|
|
@@ -1273,6 +1283,7 @@ def does_selector_apply(element: Element, selector: str) -> bool:
|
|
|
1273
1283
|
is_descendant_selector = bool(
|
|
1274
1284
|
re.match(regex_patterns.get("descendant_selector"), sel)
|
|
1275
1285
|
)
|
|
1286
|
+
is_at_rule = "@" in sel
|
|
1276
1287
|
|
|
1277
1288
|
# If the tag is an anchor, but the selector is not - doesn't apply
|
|
1278
1289
|
# link color can only be changed by a link selector
|
|
@@ -1348,6 +1359,10 @@ def does_selector_apply(element: Element, selector: str) -> bool:
|
|
|
1348
1359
|
applies = descendant_selector_applies(element, sel)
|
|
1349
1360
|
if applies:
|
|
1350
1361
|
break
|
|
1362
|
+
elif is_at_rule:
|
|
1363
|
+
# We should return false as it is an @rule and shouldn't
|
|
1364
|
+
# apply
|
|
1365
|
+
return False
|
|
1351
1366
|
else:
|
|
1352
1367
|
raise ValueError(f"Selector not recognized: Got {selector}")
|
|
1353
1368
|
return applies
|
|
@@ -19,7 +19,7 @@ regex_patterns: dict = {
|
|
|
19
19
|
"advanced_link_selector": r"(a[:.#\[]\w+)",
|
|
20
20
|
"attribute_selectors": r"[a-zA-Z]*\[(.*?)\]",
|
|
21
21
|
"child_combinator": r"\w+\s*>\s*\w+",
|
|
22
|
-
"class_selector": r"
|
|
22
|
+
"class_selector": r"\w*\.\w+",
|
|
23
23
|
"descendant_selector": r"\w+\s\w+",
|
|
24
24
|
"general_sibling_combinator": r"\w+\s*~\s*\w+",
|
|
25
25
|
"grouped_selector": r"\w+\s*,\s*\w+",
|
|
@@ -175,44 +175,51 @@ class Stylesheet:
|
|
|
175
175
|
self.text = code_without_comments
|
|
176
176
|
|
|
177
177
|
def __extract_nested_at_rules(self):
|
|
178
|
-
"""Pulls out any nested at-rule and stores them in a list.
|
|
178
|
+
"""Pulls out any nested at-rule and stores them in a list.
|
|
179
|
+
|
|
180
|
+
Algorithm: get # of @ signs"""
|
|
179
181
|
at_rules = []
|
|
180
182
|
non_at_rules_css = []
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
css_split = self.text.split("}}")
|
|
184
|
-
css_split = restore_braces(css_split)
|
|
184
|
+
css_code = self.text
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
# no @ sign, no at_rules - we're done
|
|
187
|
+
num_at_rules = css_code.count("@")
|
|
188
|
+
if num_at_rules == 0:
|
|
187
189
|
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
190
|
+
|
|
191
|
+
# add a marker symbols !! to indicate @ sign after split
|
|
192
|
+
css_code = css_code.replace("@", "@!!")
|
|
193
|
+
|
|
194
|
+
# everything between @ sign and }} is a nested at rule
|
|
195
|
+
css_split_at_at = css_code.split("@")
|
|
196
|
+
|
|
197
|
+
# Loop through code, each string beginning with ! is an at-rule
|
|
198
|
+
for code in css_split_at_at:
|
|
199
|
+
if code[:2] == "!!":
|
|
200
|
+
code = code.replace("!!", "@")
|
|
201
|
+
|
|
202
|
+
# get a slice up to }} (end of @rule)
|
|
203
|
+
code_split = code.split("}}")
|
|
204
|
+
|
|
205
|
+
# first element is the @rule
|
|
206
|
+
at_rule = code_split[0] + "}}"
|
|
207
|
+
|
|
208
|
+
# create a nested at-rule object
|
|
209
|
+
pos = at_rule.find("{")
|
|
210
|
+
rule = at_rule[:pos]
|
|
211
|
+
ruleset_string = at_rule[pos + 1 : -1]
|
|
212
|
+
nested = NestedAtRule(rule, ruleset_string)
|
|
213
|
+
if nested.has_repeat_selectors:
|
|
214
|
+
self.has_repeat_selectors = True
|
|
215
|
+
at_rules.append(nested)
|
|
216
|
+
|
|
217
|
+
# second element is any other CSS code
|
|
218
|
+
non_at_rule = code_split[1]
|
|
219
|
+
if non_at_rule:
|
|
220
|
+
non_at_rules_css.append(non_at_rule)
|
|
221
|
+
else:
|
|
222
|
+
non_at_rules_css.append(code)
|
|
216
223
|
|
|
217
224
|
self.text = "".join(non_at_rules_css)
|
|
218
225
|
self.nested_at_rules = at_rules
|
|
@@ -273,10 +280,22 @@ class Stylesheet:
|
|
|
273
280
|
):
|
|
274
281
|
selector = ruleset.selector
|
|
275
282
|
for declaration in ruleset.declaration_block.declarations:
|
|
276
|
-
|
|
283
|
+
color_counts = (
|
|
277
284
|
"color" in declaration.property
|
|
278
285
|
or "background" in declaration.property
|
|
279
|
-
|
|
286
|
+
and declaration.property
|
|
287
|
+
not in [
|
|
288
|
+
"border-color",
|
|
289
|
+
"outline-color",
|
|
290
|
+
"text-declaration-color",
|
|
291
|
+
"text-emphasis-color",
|
|
292
|
+
"text-shadow",
|
|
293
|
+
"caret-color",
|
|
294
|
+
"column-rule-color",
|
|
295
|
+
"print-color-adjust",
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
if color_counts:
|
|
280
299
|
property = declaration.property
|
|
281
300
|
value = declaration.value
|
|
282
301
|
|
|
@@ -2548,6 +2567,8 @@ def get_element_rulesets(project_dir: str, element: str) -> list:
|
|
|
2548
2567
|
filename = clerk.get_file_name(filepath)
|
|
2549
2568
|
sheets = file.get("stylesheets")
|
|
2550
2569
|
elements = html_tools.get_elements(element, filepath)
|
|
2570
|
+
if not elements:
|
|
2571
|
+
return []
|
|
2551
2572
|
for sheet in sheets:
|
|
2552
2573
|
for ruleset in sheet.rulesets:
|
|
2553
2574
|
sel = ruleset.selector
|
|
@@ -2563,6 +2584,25 @@ def get_element_rulesets(project_dir: str, element: str) -> list:
|
|
|
2563
2584
|
def get_properties_applied_report(project_dir: str, goals: dict) -> list:
|
|
2564
2585
|
"""returns a report on any elements that fail to have a property applied
|
|
2565
2586
|
|
|
2587
|
+
goals should be a dictionary with a key for each element we are checking.
|
|
2588
|
+
Each key has as it's value a dictionary with 1 to 3 possible keys...
|
|
2589
|
+
1. (required) properties (the properties that should be applied)
|
|
2590
|
+
2. (optional) min_required: If min_required is specified, then to pass
|
|
2591
|
+
it only requires the minimum number of properties to be present.
|
|
2592
|
+
|
|
2593
|
+
Sample goals might look like the following:
|
|
2594
|
+
goals_simple = {
|
|
2595
|
+
"figure": {
|
|
2596
|
+
"properties": ("box-shadow", "border-radius", "animation"),
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
goals_complex = {
|
|
2600
|
+
"figure": {
|
|
2601
|
+
"properties": ("box-shadow", "border-radius", "animation"),
|
|
2602
|
+
"min_required": 2,
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2566
2606
|
Args:
|
|
2567
2607
|
project_dir: the path to the project folder
|
|
2568
2608
|
goals: a dictionary of elements and the properties expected to be
|
|
@@ -2576,48 +2616,172 @@ def get_properties_applied_report(project_dir: str, goals: dict) -> list:
|
|
|
2576
2616
|
elements = list(goals.keys())
|
|
2577
2617
|
for element in elements:
|
|
2578
2618
|
properties_found = []
|
|
2579
|
-
|
|
2580
|
-
|
|
2619
|
+
details = goals.get(element)
|
|
2620
|
+
min_required = 0
|
|
2621
|
+
# details might be a tuple or a dictionary
|
|
2622
|
+
if isinstance(details, tuple):
|
|
2623
|
+
properties = details
|
|
2624
|
+
min_required = len(properties)
|
|
2625
|
+
else:
|
|
2626
|
+
properties = details.get("properties")
|
|
2627
|
+
min_required = details.get("min_required")
|
|
2581
2628
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
for file, rulesets in element_rulesets:
|
|
2629
|
+
html_files = get_styles_by_html_files(project_dir)
|
|
2630
|
+
for file in html_files:
|
|
2585
2631
|
found_properties_remaining = list(properties)
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
for
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2632
|
+
file_path = file.get("file")
|
|
2633
|
+
targetted_elements_in_file = html_tools.get_elements(
|
|
2634
|
+
element, file_path
|
|
2635
|
+
)
|
|
2636
|
+
# check all selectors in all stylesheet objects
|
|
2637
|
+
for sheet in file.get("stylesheets"):
|
|
2638
|
+
for rule in sheet.rulesets:
|
|
2639
|
+
selector = rule.selector
|
|
2640
|
+
selector_type = get_selector_type(selector)
|
|
2641
|
+
if selector_type == "type_selector":
|
|
2642
|
+
if selector == element:
|
|
2643
|
+
# loop through all properties and take what we can
|
|
2644
|
+
take_targetted_properties(
|
|
2645
|
+
properties_found,
|
|
2646
|
+
properties,
|
|
2647
|
+
found_properties_remaining,
|
|
2648
|
+
rule,
|
|
2649
|
+
)
|
|
2650
|
+
if selector_type == "descendant_selector":
|
|
2651
|
+
sel_split = selector.split()
|
|
2652
|
+
target = sel_split[-1]
|
|
2653
|
+
if element == target:
|
|
2654
|
+
take_targetted_properties(
|
|
2655
|
+
properties_found,
|
|
2656
|
+
properties,
|
|
2657
|
+
found_properties_remaining,
|
|
2658
|
+
rule,
|
|
2659
|
+
)
|
|
2660
|
+
if selector_type == "grouped_selector":
|
|
2661
|
+
sel_split = selector.split(",")
|
|
2662
|
+
if element in sel_split:
|
|
2663
|
+
take_targetted_properties(
|
|
2664
|
+
properties_found,
|
|
2665
|
+
properties,
|
|
2666
|
+
found_properties_remaining,
|
|
2667
|
+
rule,
|
|
2668
|
+
)
|
|
2669
|
+
if selector_type == "pseudo_selector":
|
|
2670
|
+
sel_split = selector.split(":")
|
|
2671
|
+
target = sel_split[0]
|
|
2672
|
+
if target == element:
|
|
2673
|
+
take_targetted_properties(
|
|
2674
|
+
properties_found,
|
|
2675
|
+
properties,
|
|
2676
|
+
found_properties_remaining,
|
|
2677
|
+
rule,
|
|
2678
|
+
)
|
|
2679
|
+
if selector_type == "id_selector":
|
|
2680
|
+
# could be starting with # or have tag, then hash
|
|
2681
|
+
# tag, then ID
|
|
2682
|
+
the_tag, its_id = selector.split("#")
|
|
2683
|
+
for target in targetted_elements_in_file:
|
|
2684
|
+
# our target must have an id attribute or no match
|
|
2685
|
+
if target.attrs:
|
|
2686
|
+
target_id = target.attrs.get("id")
|
|
2687
|
+
if target_id:
|
|
2688
|
+
a_match = ""
|
|
2689
|
+
if the_tag:
|
|
2690
|
+
a_match = element + "#" + target_id
|
|
2691
|
+
else:
|
|
2692
|
+
a_match = "#" + target_id
|
|
2693
|
+
if selector == a_match:
|
|
2694
|
+
take_targetted_properties(
|
|
2695
|
+
properties_found,
|
|
2696
|
+
properties,
|
|
2697
|
+
found_properties_remaining,
|
|
2698
|
+
rule,
|
|
2699
|
+
)
|
|
2700
|
+
if selector_type == "class_selector":
|
|
2701
|
+
# is the first part the tag?
|
|
2702
|
+
|
|
2703
|
+
selector_split = selector.split(".")
|
|
2704
|
+
selector_tag = ""
|
|
2705
|
+
if selector[0] != ".":
|
|
2706
|
+
selector_tag = selector_split[0]
|
|
2707
|
+
its_classes = selector_split[1:]
|
|
2708
|
+
for target in targetted_elements_in_file:
|
|
2709
|
+
# our target must have attributes
|
|
2710
|
+
if target.attrs:
|
|
2711
|
+
target_classes = target.attrs.get("class")
|
|
2712
|
+
if target_classes:
|
|
2713
|
+
# We need to sort both and compare
|
|
2714
|
+
its_classes.sort()
|
|
2715
|
+
target_classes.sort()
|
|
2716
|
+
if its_classes == target_classes:
|
|
2717
|
+
# could be a match
|
|
2718
|
+
if selector_tag:
|
|
2719
|
+
# but not if the selector
|
|
2720
|
+
# doesn't match
|
|
2721
|
+
if selector_tag != element:
|
|
2722
|
+
continue
|
|
2723
|
+
# It's a match
|
|
2724
|
+
take_targetted_properties(
|
|
2725
|
+
properties_found,
|
|
2726
|
+
properties,
|
|
2727
|
+
found_properties_remaining,
|
|
2728
|
+
rule,
|
|
2729
|
+
)
|
|
2598
2730
|
|
|
2599
2731
|
# If there are any properties left, it's a fail
|
|
2732
|
+
filename = clerk.get_file_name(file.get("file"))
|
|
2600
2733
|
if found_properties_remaining:
|
|
2601
2734
|
count = len(found_properties_remaining)
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2735
|
+
if min_required:
|
|
2736
|
+
applied = len(properties) - count
|
|
2737
|
+
if applied >= min_required:
|
|
2738
|
+
msg = f"pass: in {filename} the {element} tag applies"
|
|
2739
|
+
msg += " minimum required properties ("
|
|
2740
|
+
msg += f"{min_required})."
|
|
2741
|
+
else:
|
|
2742
|
+
msg = f"fail: in {filename}, the {element} tag only "
|
|
2743
|
+
msg += f" applied {applied} properties out of "
|
|
2744
|
+
msg += f" {min_required} required properties."
|
|
2605
2745
|
else:
|
|
2606
|
-
msg
|
|
2607
|
-
msg +=
|
|
2746
|
+
msg = f"fail: in {filename}, the {element} tag does not "
|
|
2747
|
+
msg += "apply "
|
|
2748
|
+
if count == 1:
|
|
2749
|
+
msg += f"1 property: {found_properties_remaining[0]}."
|
|
2750
|
+
else:
|
|
2751
|
+
msg += f"{count} properties: "
|
|
2752
|
+
msg += f"{found_properties_remaining}."
|
|
2608
2753
|
|
|
2609
2754
|
# all properties were accounted for (none remaining)
|
|
2610
2755
|
else:
|
|
2611
|
-
msg = f"pass: in {
|
|
2756
|
+
msg = f"pass: in {filename}, the {element} tag applies all "
|
|
2612
2757
|
msg += "required properties."
|
|
2613
2758
|
report.append(msg)
|
|
2759
|
+
if not report:
|
|
2760
|
+
msg = f"fail: no file directly targetted the {element} tag's "
|
|
2761
|
+
msg += f"properties: {properties}."
|
|
2762
|
+
report.append(msg)
|
|
2614
2763
|
return report
|
|
2615
2764
|
|
|
2616
2765
|
|
|
2766
|
+
def take_targetted_properties(
|
|
2767
|
+
properties_found, properties, found_properties_remaining, rule
|
|
2768
|
+
):
|
|
2769
|
+
declarations = rule.declaration_block.declarations
|
|
2770
|
+
for dec in declarations:
|
|
2771
|
+
prop = dec.property
|
|
2772
|
+
if prop in properties:
|
|
2773
|
+
if found_properties_remaining:
|
|
2774
|
+
if prop in found_properties_remaining:
|
|
2775
|
+
properties_found.append(prop)
|
|
2776
|
+
found_properties_remaining.remove(prop)
|
|
2777
|
+
|
|
2778
|
+
|
|
2617
2779
|
if __name__ == "__main__":
|
|
2618
2780
|
project_folder = "tests/test_files/cascade_complexities"
|
|
2619
2781
|
goals = {
|
|
2620
|
-
"figure":
|
|
2782
|
+
"figure": {
|
|
2783
|
+
"properties": ("box-shadow", "border-radius", "animation"),
|
|
2784
|
+
}
|
|
2621
2785
|
}
|
|
2622
2786
|
report = get_properties_applied_report(project_folder, goals)
|
|
2623
2787
|
rulesets = get_element_rulesets(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|