webcode-tk 1.1.4__tar.gz → 1.1.6__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: 1.1.4
3
+ Version: 1.1.6
4
4
  Summary:
5
5
  Home-page: https://github.com/HundredVisionsGuy/webcode-tk
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "webcode-tk"
3
- version = "1.1.4"
3
+ version = "1.1.6"
4
4
  description = ""
5
5
  authors = ["hundredvisionsguy <cwinikka@gmail.com>"]
6
6
  keywords = ["html", "validation", "css", ]
@@ -9,6 +9,7 @@ Modules exported by this package:
9
9
  properties and hex and rgb values.
10
10
  - `color_tools`: processes CSS color related properties and values.
11
11
  - `css_tools`: creates Stylesheet objects that store CSS information.
12
+ - `font_tools`: A collection of functions used to process font-related styles.
12
13
  - `html_tools`: gets html files from a project folder, gets the HTML code
13
14
  from files, gets number of a particular element in a file
14
15
  or folder, gets elements as tags, and much more.
@@ -18,4 +19,4 @@ Modules exported by this package:
18
19
  - `validator_tools`: sends HTML or CSS code to the W3C Validator to check
19
20
  for errors.
20
21
  """
21
- __version__ = "1.1.4"
22
+ __version__ = "1.1.6"
@@ -1273,6 +1273,7 @@ def does_selector_apply(element: Element, selector: str) -> bool:
1273
1273
  is_descendant_selector = bool(
1274
1274
  re.match(regex_patterns.get("descendant_selector"), sel)
1275
1275
  )
1276
+ is_at_rule = "@" in sel
1276
1277
 
1277
1278
  # If the tag is an anchor, but the selector is not - doesn't apply
1278
1279
  # link color can only be changed by a link selector
@@ -1348,6 +1349,10 @@ def does_selector_apply(element: Element, selector: str) -> bool:
1348
1349
  applies = descendant_selector_applies(element, sel)
1349
1350
  if applies:
1350
1351
  break
1352
+ elif is_at_rule:
1353
+ # We should return false as it is an @rule and shouldn't
1354
+ # apply
1355
+ return False
1351
1356
  else:
1352
1357
  raise ValueError(f"Selector not recognized: Got {selector}")
1353
1358
  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"\.\w+",
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
- # split at the double }} (end of a nested at rule)
183
- css_split = self.text.split("}}")
184
- css_split = restore_braces(css_split)
184
+ css_code = self.text
185
185
 
186
- if len(css_split) == 1:
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
- for code in css_split:
189
- # continue if empty
190
- if not code.strip():
191
- continue
192
- for rule in nested_at_rules:
193
- # if there is a nested @rule
194
- # split code from @rule
195
- if rule in code:
196
- split_code = code.split(rule)
197
- if len(split_code) == 2:
198
- if split_code[0]:
199
- # an @rule was NOT at the beginning or else,
200
- # there would be an empty string
201
- # that means there is CSS to add (non-at-rules)
202
- non_at_rules_css.append(split_code[0])
203
-
204
- # create a nested at-rule object
205
- text = split_code[1]
206
- pos = text.find("{")
207
- at_rule = rule + text[:pos]
208
- ruleset_string = text[pos + 1 : -1]
209
- nested = NestedAtRule(at_rule, ruleset_string)
210
- if nested.has_repeat_selectors:
211
- self.has_repeat_selectors = True
212
- at_rules.append(nested)
213
- else:
214
- # it's only an @rule
215
- print("skipping non-nested @rule.")
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
@@ -2548,6 +2555,8 @@ def get_element_rulesets(project_dir: str, element: str) -> list:
2548
2555
  filename = clerk.get_file_name(filepath)
2549
2556
  sheets = file.get("stylesheets")
2550
2557
  elements = html_tools.get_elements(element, filepath)
2558
+ if not elements:
2559
+ return []
2551
2560
  for sheet in sheets:
2552
2561
  for ruleset in sheet.rulesets:
2553
2562
  sel = ruleset.selector
@@ -2563,6 +2572,25 @@ def get_element_rulesets(project_dir: str, element: str) -> list:
2563
2572
  def get_properties_applied_report(project_dir: str, goals: dict) -> list:
2564
2573
  """returns a report on any elements that fail to have a property applied
2565
2574
 
2575
+ goals should be a dictionary with a key for each element we are checking.
2576
+ Each key has as it's value a dictionary with 1 to 3 possible keys...
2577
+ 1. (required) properties (the properties that should be applied)
2578
+ 2. (optional) min_required: If min_required is specified, then to pass
2579
+ it only requires the minimum number of properties to be present.
2580
+
2581
+ Sample goals might look like the following:
2582
+ goals_simple = {
2583
+ "figure": {
2584
+ "properties": ("box-shadow", "border-radius", "animation"),
2585
+ }
2586
+ }
2587
+ goals_complex = {
2588
+ "figure": {
2589
+ "properties": ("box-shadow", "border-radius", "animation"),
2590
+ "min_required": 2,
2591
+ }
2592
+ }
2593
+
2566
2594
  Args:
2567
2595
  project_dir: the path to the project folder
2568
2596
  goals: a dictionary of elements and the properties expected to be
@@ -2576,48 +2604,172 @@ def get_properties_applied_report(project_dir: str, goals: dict) -> list:
2576
2604
  elements = list(goals.keys())
2577
2605
  for element in elements:
2578
2606
  properties_found = []
2579
- properties = goals.get(element)
2580
- element_rulesets = get_element_rulesets(project_dir, element)
2607
+ details = goals.get(element)
2608
+ min_required = 0
2609
+ # details might be a tuple or a dictionary
2610
+ if isinstance(details, tuple):
2611
+ properties = details
2612
+ min_required = len(properties)
2613
+ else:
2614
+ properties = details.get("properties")
2615
+ min_required = details.get("min_required")
2581
2616
 
2582
- # check all rulesets for a given file to see which rulesets have
2583
- # one or more properties we are checking for
2584
- for file, rulesets in element_rulesets:
2617
+ html_files = get_styles_by_html_files(project_dir)
2618
+ for file in html_files:
2585
2619
  found_properties_remaining = list(properties)
2586
- for expected_property in properties:
2587
- declarations = rulesets.declaration_block.declarations
2588
-
2589
- # check all declarations to see if any properties
2590
- # are a match and pull it from the list of remaining
2591
- # properties.
2592
- for declaration in declarations:
2593
- element_property = declaration.property
2594
- if expected_property == element_property:
2595
- properties_found.append(element_property)
2596
- found_properties_remaining.remove(element_property)
2597
- break
2620
+ file_path = file.get("file")
2621
+ targetted_elements_in_file = html_tools.get_elements(
2622
+ element, file_path
2623
+ )
2624
+ # check all selectors in all stylesheet objects
2625
+ for sheet in file.get("stylesheets"):
2626
+ for rule in sheet.rulesets:
2627
+ selector = rule.selector
2628
+ selector_type = get_selector_type(selector)
2629
+ if selector_type == "type_selector":
2630
+ if selector == element:
2631
+ # loop through all properties and take what we can
2632
+ take_targetted_properties(
2633
+ properties_found,
2634
+ properties,
2635
+ found_properties_remaining,
2636
+ rule,
2637
+ )
2638
+ if selector_type == "descendant_selector":
2639
+ sel_split = selector.split()
2640
+ target = sel_split[-1]
2641
+ if element == target:
2642
+ take_targetted_properties(
2643
+ properties_found,
2644
+ properties,
2645
+ found_properties_remaining,
2646
+ rule,
2647
+ )
2648
+ if selector_type == "grouped_selector":
2649
+ sel_split = selector.split(",")
2650
+ if element in sel_split:
2651
+ take_targetted_properties(
2652
+ properties_found,
2653
+ properties,
2654
+ found_properties_remaining,
2655
+ rule,
2656
+ )
2657
+ if selector_type == "pseudo_selector":
2658
+ sel_split = selector.split(":")
2659
+ target = sel_split[0]
2660
+ if target == element:
2661
+ take_targetted_properties(
2662
+ properties_found,
2663
+ properties,
2664
+ found_properties_remaining,
2665
+ rule,
2666
+ )
2667
+ if selector_type == "id_selector":
2668
+ # could be starting with # or have tag, then hash
2669
+ # tag, then ID
2670
+ the_tag, its_id = selector.split("#")
2671
+ for target in targetted_elements_in_file:
2672
+ # our target must have an id attribute or no match
2673
+ if target.attrs:
2674
+ target_id = target.attrs.get("id")
2675
+ if target_id:
2676
+ a_match = ""
2677
+ if the_tag:
2678
+ a_match = element + "#" + target_id
2679
+ else:
2680
+ a_match = "#" + target_id
2681
+ if selector == a_match:
2682
+ take_targetted_properties(
2683
+ properties_found,
2684
+ properties,
2685
+ found_properties_remaining,
2686
+ rule,
2687
+ )
2688
+ if selector_type == "class_selector":
2689
+ # is the first part the tag?
2690
+
2691
+ selector_split = selector.split(".")
2692
+ selector_tag = ""
2693
+ if selector[0] != ".":
2694
+ selector_tag = selector_split[0]
2695
+ its_classes = selector_split[1:]
2696
+ for target in targetted_elements_in_file:
2697
+ # our target must have attributes
2698
+ if target.attrs:
2699
+ target_classes = target.attrs.get("class")
2700
+ if target_classes:
2701
+ # We need to sort both and compare
2702
+ its_classes.sort()
2703
+ target_classes.sort()
2704
+ if its_classes == target_classes:
2705
+ # could be a match
2706
+ if selector_tag:
2707
+ # but not if the selector
2708
+ # doesn't match
2709
+ if selector_tag != element:
2710
+ continue
2711
+ # It's a match
2712
+ take_targetted_properties(
2713
+ properties_found,
2714
+ properties,
2715
+ found_properties_remaining,
2716
+ rule,
2717
+ )
2598
2718
 
2599
2719
  # If there are any properties left, it's a fail
2720
+ filename = clerk.get_file_name(file.get("file"))
2600
2721
  if found_properties_remaining:
2601
2722
  count = len(found_properties_remaining)
2602
- msg = f"fail: in {file}, the {element} tag does not apply "
2603
- if count == 1:
2604
- msg += f"1 property: {found_properties_remaining[0]}."
2723
+ if min_required:
2724
+ applied = len(properties) - count
2725
+ if applied >= min_required:
2726
+ msg = f"pass: in {filename} the {element} tag applies"
2727
+ msg += " minimum required properties ("
2728
+ msg += f"{min_required})."
2729
+ else:
2730
+ msg = f"fail: in {filename}, the {element} tag only "
2731
+ msg += f" applied {applied} properties out of "
2732
+ msg += f" {min_required} required properties."
2605
2733
  else:
2606
- msg += f"{count} properties: "
2607
- msg += f"{found_properties_remaining}."
2734
+ msg = f"fail: in {filename}, the {element} tag does not "
2735
+ msg += "apply "
2736
+ if count == 1:
2737
+ msg += f"1 property: {found_properties_remaining[0]}."
2738
+ else:
2739
+ msg += f"{count} properties: "
2740
+ msg += f"{found_properties_remaining}."
2608
2741
 
2609
2742
  # all properties were accounted for (none remaining)
2610
2743
  else:
2611
- msg = f"pass: in {file}, the {element} tag applies all "
2744
+ msg = f"pass: in {filename}, the {element} tag applies all "
2612
2745
  msg += "required properties."
2613
2746
  report.append(msg)
2747
+ if not report:
2748
+ msg = f"fail: no file directly targetted the {element} tag's "
2749
+ msg += f"properties: {properties}."
2750
+ report.append(msg)
2614
2751
  return report
2615
2752
 
2616
2753
 
2754
+ def take_targetted_properties(
2755
+ properties_found, properties, found_properties_remaining, rule
2756
+ ):
2757
+ declarations = rule.declaration_block.declarations
2758
+ for dec in declarations:
2759
+ prop = dec.property
2760
+ if prop in properties:
2761
+ if found_properties_remaining:
2762
+ if prop in found_properties_remaining:
2763
+ properties_found.append(prop)
2764
+ found_properties_remaining.remove(prop)
2765
+
2766
+
2617
2767
  if __name__ == "__main__":
2618
2768
  project_folder = "tests/test_files/cascade_complexities"
2619
2769
  goals = {
2620
- "figure": ("margin", "padding", "border", "float"),
2770
+ "figure": {
2771
+ "properties": ("box-shadow", "border-radius", "animation"),
2772
+ }
2621
2773
  }
2622
2774
  report = get_properties_applied_report(project_folder, goals)
2623
2775
  rulesets = get_element_rulesets(
@@ -22,6 +22,7 @@ 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
  import re
25
+ from typing import Any
25
26
  from typing import Union
26
27
 
27
28
  from webcode_tk import css_tools
@@ -76,9 +77,7 @@ FONT_NON_SIZE_NON_FAMILY_KEYWORDS = (
76
77
  )
77
78
 
78
79
 
79
- def get_absolute_keyword_values(
80
- css_code: Union[str, css_tools.Stylesheet]
81
- ) -> list:
80
+ def get_absolute_keyword_values(css_code: any) -> list:
82
81
  """returns a list of all keyword values in CSS
83
82
 
84
83
  To be safe, we should remove all selectors using the curly brackets
@@ -110,9 +109,7 @@ def get_absolute_keyword_values(
110
109
  return values
111
110
 
112
111
 
113
- def get_numeric_fontsize_values(
114
- css_code: Union[str, css_tools.Stylesheet]
115
- ) -> list:
112
+ def get_numeric_fontsize_values(css_code: Any) -> list:
116
113
  """returns a list of all numeric font size values.
117
114
 
118
115
  This should work for any standard size value (em, px, %,
@@ -121,10 +118,10 @@ def get_numeric_fontsize_values(
121
118
  Args:
122
119
  css_code: the CSS styles in either string or Stylesheet format.
123
120
  """
124
- if isinstance(css_code, css_tools.Stylesheet):
125
- code = css_code.text
126
- else:
121
+ if isinstance(css_code, str):
127
122
  code = css_code
123
+ else:
124
+ code = css_code.text
128
125
  matches = re.findall(numeric_value_regex, code)
129
126
  full_matches = []
130
127
  for match in matches:
@@ -1,674 +0,0 @@
1
- """ stylesheet
2
- This module is a class to define CSS stylesheets for use in css_tools and
3
- others.
4
- """
5
- import re
6
-
7
- from webcode_tk import color_tools
8
- from webcode_tk import css_tools
9
-
10
-
11
- # regex patterns for various selectors
12
- # on attribute selector, if you want
13
- regex_patterns: dict = {
14
- "adjacent_sibling_combinator": r"\w+\s*\+\s*\w+",
15
- "advanced_link_selector": r"(a[:.#\[]\w+)",
16
- "attribute_selectors": r"[a-zA-Z]*\[(.*?)\]",
17
- "child_combinator": r"\w+\s*>\s*\w+",
18
- "class_selector": r"\.\w+",
19
- "descendant_selector": r"\w+\s\w+",
20
- "general_sibling_combinator": r"\w+\s*~\s*\w+",
21
- "grouped_selector": r"\w+\s*,\s*\w+",
22
- "header_selector": r"h[1-6]",
23
- "id_selector": r"(.*?)#[a-zA-Z0-9-_.:]+",
24
- "pseudoclass_selector": r"(?<!:):\w+",
25
- "pseudoelement_selector": r"(\w+)?::\w+(-\w+)?",
26
- "single_attribute_selector": r"^[a-zA-Z]*\[(.*?)\]",
27
- "single_type_selector": r"^[a-zA-Z][a-zA-Z0-9]*$",
28
- "type_selector": r"(?:^|\s)([a-zA-Z][a-zA-Z0-9_-]*)",
29
- "vendor_prefix": r"\A-moz-|-webkit-|-ms-|-o-",
30
- }
31
-
32
- # all relevant at-rules.
33
- # from the Mozilla Developer Network's article, At-rules
34
- # https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
35
- nested_at_rules: tuple = (
36
- "@supports",
37
- "@document",
38
- "@page",
39
- "@font-face",
40
- "@keyframes",
41
- "@media",
42
- "@viewport",
43
- "@counter-style",
44
- "@font-feature-values",
45
- "@property",
46
- )
47
-
48
-
49
- class Stylesheet:
50
- """A Stylesheet object with details about the sheet and its
51
- components.
52
-
53
- The stylesheet object has the full code, a list of comments from the
54
- stylesheet, a list of nested @rules, rulesets pertaining to colors,
55
- a list of all selectors, and information about repeated selectors.
56
-
57
- About repeated selectors, front-end developers should always employ
58
- the DRY principle: Don't Repeat Yourself. In other words, if you
59
- use a selector once in your stylesheet, the only other place you
60
- would logically put the same selector would be in a nested at-rule
61
- (in particular, an @media or @print breakpoint)
62
-
63
- For this reason, both the Stylesheet object and the NesteAtRule
64
- objects have attributes that show whether there are repeated
65
- selectors or not as well as which selectors get repeated.
66
-
67
- Attributes:
68
- href: the filename (not path), which may end with .css or .html
69
- (if stylesheet object comes from a style tag).
70
- text: the actual code itself of the entire file or style tag.
71
- type: whether it's a file or local if it's from an style tag.
72
- nested_at_rules: a list of all nested at-rules.
73
- rulesets: a list of all rulesets.
74
- comments: a list of all comments in string format.
75
- color_rulesets: a list of all rulesets that target color or
76
- background colors.
77
- selectors: a list of all selectors.
78
- has_repeat_selectors (bool): whether there are any repeated
79
- selectors anywhere in the stylesheet (including in the
80
- NestedAtRule.
81
- repeated_selectors (list): a list of any selectors that are
82
- repeated. They might be repeated in the main stylesheet
83
- or they might be repeated in one of the nested @rules.
84
- """
85
-
86
- def __init__(
87
- self, href: str, text: str, stylesheet_type: str = "file"
88
- ) -> None:
89
- """Inits Stylesheet with href, text (CSS code), and type."""
90
- self.type = stylesheet_type
91
- self.href = href
92
- self.text = text
93
- self.__clean_text()
94
- self.nested_at_rules = []
95
- self.rulesets = []
96
- self.comments = []
97
- self.color_rulesets = []
98
- self.selectors = []
99
- self.has_repeat_selectors = False
100
- self.repeated_selectors = []
101
- self.__minify()
102
- self.__replace_variables()
103
- self.__remove_external_imports()
104
- self.__extract_comments()
105
- self.__extract_nested_at_rules()
106
- self.__extract_rulesets()
107
- self.__set_selectors()
108
-
109
- def __clean_text(self):
110
- """cleans up CSS like removing extra line returns
111
-
112
- This is here because if a student has more than 2 blank lines, it
113
- could trigger an attribute error (at least it did in the past)
114
- """
115
- text_to_clean = self.text
116
- split_text = text_to_clean.split("\n")
117
- cleaned_text = ""
118
- consecutive_blanks = 0
119
- for line in split_text:
120
- if not line:
121
- consecutive_blanks += 1
122
- if consecutive_blanks > 1:
123
- consecutive_blanks = 1
124
- continue
125
- else:
126
- if not line and cleaned_text:
127
- cleaned_text += "\n"
128
- cleaned_text += line + "\n"
129
- if cleaned_text[-1:] == "\n":
130
- cleaned_text = cleaned_text[:-1].strip()
131
- self.text = cleaned_text
132
-
133
- def __minify(self):
134
- """Removes all whitespace, line returns, and tabs from text."""
135
- self.text = css_tools.minify_code(self.text)
136
-
137
- def __replace_variables(self):
138
- """Looks for and replaces any variables set in stylesheet with
139
- the variable's values."""
140
- # get a list of all variables and their values
141
- variable_list = css_tools.get_variables(self.text)
142
-
143
- # Loop through the variable list and do a find
144
- # and replace on all occurrances of the variable
145
- new_text = self.text
146
- for variable in variable_list:
147
- var = variable.get("variable")
148
- value = variable.get("value")
149
- var = r"var\(" + var + r"\)"
150
- new_text = re.sub(var, value, new_text)
151
- self.text = new_text
152
-
153
- def __extract_comments(self):
154
- """Gets all comments from the code and stores in a list."""
155
- # split all CSS text at opening comment
156
- text_comment_split = self.text.split("/*")
157
- comments = []
158
- code_without_comments = ""
159
-
160
- # loop through the list of code
161
- # in each iteration extract the comment
162
- for i in text_comment_split:
163
- if "*/" in i:
164
- comment = i.split("*/")
165
- comments.append("/*" + comment[0] + "*/")
166
- code_without_comments += comment[1]
167
- else:
168
- # no comments, just get code
169
- code_without_comments += i
170
- self.comments = comments
171
- self.text = code_without_comments
172
-
173
- def __extract_nested_at_rules(self):
174
- """Pulls out any nested at-rule and stores them in a list."""
175
- at_rules = []
176
- non_at_rules_css = []
177
-
178
- # split at the double }} (end of a nested at rule)
179
- css_split = self.text.split("}}")
180
- css_split = css_tools.restore_braces(css_split)
181
-
182
- if len(css_split) == 1:
183
- return
184
- for code in css_split:
185
- # continue if empty
186
- if not code.strip():
187
- continue
188
- for rule in nested_at_rules:
189
- # if there is a nested @rule
190
- # split code from @rule
191
- if rule in code:
192
- split_code = code.split(rule)
193
- if len(split_code) == 2:
194
- if split_code[0]:
195
- # an @rule was NOT at the beginning or else,
196
- # there would be an empty string
197
- # that means there is CSS to add (non-at-rules)
198
- non_at_rules_css.append(split_code[0])
199
-
200
- # create a nested at-rule object
201
- text = split_code[1]
202
- pos = text.find("{")
203
- at_rule = rule + text[:pos]
204
- ruleset_string = text[pos + 1 : -1]
205
- nested = NestedAtRule(at_rule, ruleset_string)
206
- if nested.has_repeat_selectors:
207
- self.has_repeat_selectors = True
208
- at_rules.append(nested)
209
- else:
210
- # it's only an @rule
211
- print("skipping non-nested @rule.")
212
-
213
- self.text = "".join(non_at_rules_css)
214
- self.nested_at_rules = at_rules
215
-
216
- def __extract_rulesets(self):
217
- """Separates all code into individual rulesets."""
218
- # split rulesets by closing of rulesets: }
219
- ruleset_list = self.text.split("}")
220
- for ruleset in ruleset_list:
221
- if ruleset:
222
- ruleset = Ruleset(ruleset + "}")
223
- self.rulesets.append(ruleset)
224
- self.get_color_ruleset(ruleset)
225
-
226
- def __remove_external_imports(self):
227
- text = self.text
228
- # look for external link by protocol (http or https)
229
- external_import_re = r"@import url\(['\"]https://|"
230
- external_import_re += r"@import url\(['\"]http://"
231
-
232
- # remove external imports if there's a protocol
233
- # text = text.lower()
234
- match = re.search(external_import_re, text)
235
- if match:
236
- # but only if it's in an @import url function
237
- split_text = re.split(external_import_re, text)
238
-
239
- # we now have 1 or more code segments without the
240
- # beginnings of an @import url( segment
241
- for i in range(1, len(split_text)):
242
- segment = split_text[i]
243
- # get everything after the first );
244
- paren_pos = segment.index(")") + 1
245
- segment = segment[paren_pos:]
246
- if ";" in segment[:2]:
247
- pos = segment[:2].index(";")
248
- segment = segment[pos + 1 :]
249
- split_text[i] = segment
250
- # put text back in string form
251
- text = "".join(split_text)
252
- self.text = text
253
-
254
- def get_color_ruleset(self, ruleset: "Ruleset") -> list:
255
- """Returns a list of all rules targetting color or background color.
256
-
257
- Args:
258
- ruleset(Ruleset): a Ruleset object complete with selector
259
- and declaration block.
260
-
261
- Returns:
262
- color_rulesets: a list of all selectors that target color
263
- in some way, but just with the color-based declarations.
264
- """
265
- color_rulesets = []
266
- if ruleset.declaration_block and (
267
- "color:" in ruleset.declaration_block.text
268
- or "background" in ruleset.declaration_block.text
269
- ):
270
- selector = ruleset.selector
271
- for declaration in ruleset.declaration_block.declarations:
272
- if (
273
- "color" in declaration.property
274
- or "background" in declaration.property
275
- ):
276
- property = declaration.property
277
- value = declaration.value
278
-
279
- # Check for a gradient bg color
280
- is_bg_gradient = color_tools.is_gradient(value)
281
- if is_bg_gradient:
282
- print()
283
- # skip if has vendor prefix
284
- if css_tools.has_vendor_prefix(value):
285
- continue
286
- # skip if not valid color value
287
- is_valid_color = color_tools.is_color_value(value)
288
- if not is_valid_color and not is_bg_gradient:
289
- continue
290
- # make sure the value is a color (not other)
291
- rule = {selector: {property: value}}
292
- color_rulesets.append(rule)
293
- if color_rulesets:
294
- self.color_rulesets += color_rulesets
295
-
296
- def __set_selectors(self):
297
- """Adds all selectors from stylesheet to selectors attribute."""
298
- for rule in self.rulesets:
299
- if rule.selector in self.selectors:
300
- self.has_repeat_selectors = True
301
- self.repeated_selectors.append(rule.selector)
302
- self.selectors.append(rule.selector)
303
-
304
- def sort_selectors(self):
305
- """Puts all selectors in alphabetical order."""
306
- self.selectors.sort()
307
-
308
-
309
- class NestedAtRule:
310
- """An at-rule rule that is nested, such as @media or @keyframes.
311
-
312
- Nested at-rules include animation keyframes, styles for print
313
- (@media print), and breakpoints (@media screen). Each nested
314
- at-rule has an at-rule, which works like a selector, and a
315
- ruleset for that at-rule. The ruleset may contain any number
316
- of selectors and their declaration blocks.
317
-
318
- You can almost think of them as stylesheets within a stylesheet
319
- *"A dweam within a dweam"* -The Impressive Clergyman.
320
- *"We have to go deeper"* -Dom Cobb.
321
-
322
- Nested at-rules are defined in the global variable: nested_at_rules.
323
- For more information on nested at-rules, you want to refer to MDN's
324
- [nested]
325
- (https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule#nested)
326
-
327
- Args:
328
- at_rule (str): the full at-rule such as '@media only and
329
- (min-width: 520px)'.
330
- text (str): the text of the code (without the at_rule).
331
- Provide the text if you do not provide a list of rulesets.
332
- rules (list): a list of Ruleset objects. This is optional and
333
- defaults to None. Just be sure to add text if you don't
334
- provide a list.
335
- Attributes:
336
- at_rule (str): the full at-rule such as '@media only and
337
- (min-width: 520px)'.
338
- rulesets (list): a list of Ruleset objects.
339
- selectors (list): a list of all selectors from the rulesets
340
- has_repeat_selectors (bool): whether there are any repeated
341
- selectors in the NestedAtRule.
342
- repeated_selectors (list): a list of any selectors that are
343
- repeated.
344
- """
345
-
346
- def __init__(self, at_rule, text="", rules=None):
347
- """Inits a Nested @rule object.
348
-
349
- Raises:
350
- ValueError: an error is raised if neither at_rule nor text is
351
- provided for the constructor or both are provided but they
352
- do not match.
353
- """
354
- self.at_rule = at_rule.strip()
355
- if rules is None:
356
- self.rulesets = []
357
- else:
358
- self.rulesets = rules[:]
359
- self.selectors = []
360
- self.has_repeat_selectors = False
361
- self.repeated_selectors = []
362
-
363
- # If rulesets were NOT passed in, we need to get them from the text
364
- if not rules:
365
- self.set_rulesets(text)
366
- else:
367
- # if both rules and text were passed in make sure they
368
- # match and raise a ValueError if not
369
- if rules and text:
370
- code_split = text.split("}")
371
- if len(code_split) != len(rules):
372
- msg = "You passed both a ruleset and text, but "
373
- msg += "The text does not match the rules"
374
- raise ValueError(msg)
375
- # let's get our selectors
376
- for rule in self.rulesets:
377
- selector = rule.selector
378
- self.selectors.append(selector)
379
- self.check_repeat_selectors()
380
-
381
- def check_repeat_selectors(self):
382
- """Checks to see if there are any repeated selectors"""
383
- for selector in self.selectors:
384
- count = self.selectors.count(selector)
385
- if count > 1:
386
- self.has_repeat_selectors = True
387
- self.repeated_selectors.append(selector)
388
-
389
- def set_rulesets(self, text):
390
- """Converts string of text into a list of ruleset objects"""
391
- # first, make sure text was not an empty string
392
- if text.strip():
393
- self.__text = css_tools.minify_code(text)
394
- else:
395
- msg = "A NestedAtRule must be provided either rulesets"
396
- msg += " or text, but you provided no useable code."
397
- raise ValueError(msg)
398
- if self.__text.count("}") == 1:
399
- ruleset = Ruleset(self.__text)
400
- self.selectors.append(ruleset.selector)
401
- self.rulesets.append(ruleset)
402
- else:
403
- code_split = self.__text.split("}")
404
- rulesets = []
405
- for part in code_split:
406
- if part.strip():
407
- ruleset = Ruleset(part + "}")
408
- if ruleset:
409
- selector = ruleset.selector
410
- self.selectors.append(selector)
411
- rulesets.append(ruleset)
412
- if rulesets:
413
- self.rulesets = rulesets
414
-
415
-
416
- class Ruleset:
417
- """Creates a ruleset: a selector with a declaration block.
418
-
419
- For more information about Rulesets, please read MDN's article on
420
- [Rulesets]
421
- (https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax#css_rulesets)
422
-
423
- Args:
424
- text (str): the CSS code in text form.
425
-
426
- Attributes:
427
- __text (str): the CSS code.
428
- selector (str): the selector of the Ruleset
429
- declaration_block (DeclarationBlock): a DeclarationBlock
430
- object.
431
- is_valid (bool): whether the Ruleset is valid or not.
432
- """
433
-
434
- def __init__(self, text):
435
- """Inits a DeclarationBlock object using CSS code"""
436
- self.__text = text
437
- self.selector = ""
438
- self.declaration_block = None
439
- self.is_valid = True
440
- self.validate()
441
- self.initialize()
442
-
443
- def initialize(self):
444
- """converts the text into a DeclarationBlock."""
445
- if self.is_valid:
446
- contents = self.__text.split("{")
447
- self.selector = contents[0].replace("\n", "").strip()
448
- block = contents[1].replace("\n", "")
449
- self.declaration_block = DeclarationBlock(block)
450
-
451
- def validate(self):
452
- """Determines whether the code is valid or not"""
453
- try:
454
- open_brace_pos = self.__text.index("{")
455
- close_brace_pos = self.__text.index("}")
456
- if open_brace_pos > close_brace_pos:
457
- # { needs to come before }
458
- self.is_valid = False
459
- except Exception:
460
- self.is_valid = False
461
-
462
- if "{" not in self.__text or "}" not in self.__text:
463
- self.is_valid = False
464
-
465
-
466
- class DeclarationBlock:
467
- """A set of properties and values that go with a selector
468
-
469
- In CSS a declaration block is a block of code set off by curly
470
- brackets `{}`. They come after a selector and contain one or more
471
- declarations (pairs of properties and values such as
472
- `width: 200px`).
473
-
474
- Attributes:
475
- text (str): full text of the declaration block including
476
- curly brackets.
477
- declarations: a list of Declaration objects (see the
478
- Declaration class below)."""
479
-
480
- def __init__(self, text):
481
- """Inits a declaration block"""
482
- self.text = text
483
- self.declarations = []
484
- self.__set_declarations()
485
-
486
- def __set_declarations(self):
487
- """converts text into a list of declarations."""
488
- declarations = self.text
489
-
490
- # remove selectors and braces if present
491
- if "{" in self.text:
492
- declarations = declarations.split("{")
493
- declarations = declarations[1]
494
- if "}" in declarations:
495
- declarations = declarations.split("}")
496
- declarations = declarations[0]
497
-
498
- declarations = declarations.split(";")
499
-
500
- # remove all spaces and line returns
501
- # capture positions of content we want to keep
502
- keep = []
503
- for i in range(len(declarations)):
504
- declarations[i] = declarations[i].replace("\n", "")
505
- declarations[i] = declarations[i].strip()
506
- if declarations[i]:
507
- keep.append(i)
508
-
509
- # get only declarations with content
510
- to_keep = []
511
- for pos in keep:
512
- to_keep.append(declarations[pos])
513
- declarations = to_keep
514
-
515
- # set all Declaration objects
516
- for i in range(len(declarations)):
517
- declarations[i] = Declaration(declarations[i])
518
- self.declarations = declarations
519
-
520
-
521
- class Declaration:
522
- """A property and value pair.
523
-
524
- A declaration is a pairing of a property with a specific value.
525
- Examples include: `font-family: Helvetica;` which changes the
526
- font to Helvetica. Another example could be `min-height: 100px`
527
- which sets the height of the element to be at the very least
528
- 100 pixels.
529
-
530
- Attributes:
531
- text (str): the text of the declaration in the form of
532
- `property: value;`
533
- property (str): the thing you want to change (like `color`
534
- or `border-width`.
535
- value (str): what you want to change it to (like `aquamarine`
536
- or `5px`"""
537
-
538
- def __init__(self, text):
539
- """Inits a Declaration object."""
540
- self.__text = text
541
- self.property = ""
542
- self.value = ""
543
- self.invalid_message = ""
544
- self.is_color = False
545
- # validate before trying to set the declaration.
546
- try:
547
- self.validate_declaration()
548
- self.is_valid = True
549
- self.set_declaration()
550
- self.is_color_property()
551
- except ValueError as e:
552
- self.is_valid = False
553
- self.invalid_message = str(e)
554
-
555
- def set_declaration(self):
556
- """Sets the property and value based on the text (CSS code).
557
-
558
- Note: this only gets run if the declaration was valid, and
559
- we already ran the validation. Had the code not been valid,
560
- it would have already thrown an exception, and we wouldn't
561
- be in this method."""
562
- elements = self.__text.split(":")
563
- self.property = elements[0].strip()
564
- self.value = elements[1].strip()
565
-
566
- def validate_declaration(self):
567
- """Raises a ValueError if any part of the Declaration is
568
- invalid."""
569
-
570
- # split text at colon (should have 2 items only: the property
571
- # on the left of the colon and the value on the right of the
572
- # colon)
573
- try:
574
- property, value = self.__text.split(":")
575
- except ValueError as err:
576
- if "not enough values" in str(err):
577
- # There was no colon - there must be one
578
- msg = "The code is missing a colon. All declarations "
579
- msg += "must have a colon between the property and "
580
- msg += "the value."
581
- raise ValueError(msg)
582
- elif "too many values" in str(err):
583
- # There were two or more colons - can only be one
584
- msg = "You have too many colons. There should only be "
585
- msg += "one colon between the property and the value."
586
- raise ValueError(msg)
587
-
588
- self.validate_property(property)
589
- self.validate_value(value)
590
-
591
- def validate_property(self, property) -> bool:
592
- """checks property to make sure it is a valid CSS property.
593
-
594
- A CSS property is valid if there are no spaces in between the
595
- text. In future versions, we could check against a list of
596
- valid properties, but that might take us down a rabbit hole
597
- of ever changing properties.
598
-
599
- Args:
600
- property (str): the property of the Declaration which might
601
- or might not be valid.
602
-
603
- Raises:
604
- ValueError: if the property is an invalid property
605
- """
606
-
607
- # Make sure there are no spaces in between property
608
- prop_list = property.strip().split()
609
- if len(prop_list) > 1:
610
- msg = "You cannot have a space in the middle of a property."
611
- msg += "Did you forget the dash `-`?"
612
- raise ValueError(msg)
613
-
614
- def validate_value(self, value, property=None):
615
- """Raises a ValueError if the value is invalid.
616
-
617
- Caveat: this is by no means a comprehensive validation, and
618
- so there is much room for improvement. For now, we're focusing
619
- on the basics, such as there can be no text after the semi-
620
- colon and there should be no units if the value is 0.
621
-
622
- In future versions, we could extend the validation to make
623
- sure the units match the property, which is why we added a
624
- default value for property.
625
-
626
- Args:
627
- value (str): the code after the colon (what specifically
628
- do you want the property set to)
629
- property (str): the property which defaults to None.
630
-
631
- Raises:
632
- ValueError: if the value is invalid.
633
- """
634
- if property is None:
635
- property = ""
636
-
637
- value = value.strip()
638
- # Make sure there's nothing after the semi-colon
639
- # but account for the empty string element after the split
640
- # as well as spaces (just in case)
641
- val_list = value.split(";")
642
- if len(val_list) > 1 and val_list[1].strip():
643
- msg = "There should be no text after the semi-colon."
644
- raise ValueError(msg)
645
- if value == ";" or not value:
646
- msg = "You are missing a value. You must include a "
647
- msg += "value in between the colon : and the semi-"
648
- msg += "colon ;"
649
- raise ValueError(msg)
650
- # Check for a value of 0 and make sure there are no units
651
- zero_pattern = r"^\b0\w"
652
- match = re.search(zero_pattern, value)
653
- if match:
654
- msg = "Values of 0 do not need a unit. Example: 0px should "
655
- msg += "be just 0."
656
- raise ValueError(msg)
657
-
658
- # TODO: add some validation based on property type
659
-
660
- def get_declaration(self) -> str:
661
- """Returns the declaration in the form of `property: value`
662
-
663
- Returns:
664
- declaration (str): a property and its value separated by
665
- a colon. Example: `"color: rebeccapurple"`"""
666
-
667
- declaration = self.property + ": " + self.value
668
- return declaration
669
-
670
- def is_color_property(self):
671
- value = self.value
672
- if value[-1] == ";":
673
- value = value[:-1]
674
- self.is_color = color_tools.is_color_value(value)
File without changes
File without changes