webcode-tk 1.1.4__tar.gz → 1.1.5__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.4 → webcode_tk-1.1.5}/PKG-INFO +1 -1
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/pyproject.toml +1 -1
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/__init__.py +2 -1
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/font_tools.py +6 -9
- webcode_tk-1.1.4/webcode_tk/stylesheet.py +0 -674
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/LICENSE.rst +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/README.rst +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/cascade_tools.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/color_keywords.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/color_tools.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/css_tools.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/html_tools.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/sample_report.json +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/ux_tools.py +0 -0
- {webcode_tk-1.1.4 → webcode_tk-1.1.5}/webcode_tk/validator_tools.py +0 -0
|
@@ -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.
|
|
22
|
+
__version__ = "1.1.5"
|
|
@@ -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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|