-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathcss_modules.py
124 lines (102 loc) · 4.25 KB
/
css_modules.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import re
from dataclasses import dataclass, field
from typing import Any, Literal, TypedDict
import tinycss2
from tinycss2.ast import QualifiedRule
file_text = open("example.css", "r").read()
SquareBracketValues = TypedDict(
"SquareBracketValues",
{"key": str, "operator": str | None, "value": str | list | None},
)
@dataclass
class CssRule:
"""Represents a CSS selector.
Examples of the parser's targets are shown below as docstrings."""
tag: str | Literal["*"] | None = None
"""div, span"""
id: str | None = None
"""#id1, #id2"""
classes: list[str] = field(default_factory=list)
""".class1, .class2"""
attributes: list[SquareBracketValues] = field(default_factory=list)
"""[key=value], [key^=value]"""
styles: dict[str, Any] = field(default_factory=dict)
"""color: red;"""
psuedo_functions: list[str] = field(default_factory=list)
""":not(), :nth-child()"""
psuedo_elements: list[str] = field(default_factory=list)
""":hover, :focus"""
psuedo_classes: list[str] = field(default_factory=list)
"""::before, ::after"""
next: list["CssRule"] = field(default_factory=list)
"""#current-selector #next-selector"""
next_operator: str = " "
""">, +, ~"""
@dataclass
class CssToPython:
"""Performs a best-effort conversion of a CSS file into a list of Python dictionaries."""
content: str
def convert(self) -> list[CssRule]:
"""Converts the CSS file into a list of CSS rule dictionaries."""
self.parsed: list[QualifiedRule] = tinycss2.parse_stylesheet(
self.content,
skip_comments=True,
skip_whitespace=True,
)
self.selectors: list[CssRule] = []
for style in self.parsed:
if style.type != "qualified-rule":
continue
selector = CssRule()
# Determine what CSS rules are defined in the selector
computed_rules = tinycss2.parse_declaration_list(
style.content, skip_comments=True, skip_whitespace=True
)
for rule in computed_rules:
selector.styles[rule.name] = self._parse_rule_value(rule.value)
# Parse HTML tag name from the selector
if style.prelude[0].type == "ident":
selector.tag = style.prelude[0].value
elif style.prelude[0].type == "literal" and style.prelude[0].value == "*":
selector.tag = "*"
# Parse all other attributes from the selector
print(style.prelude)
for count, token in enumerate(style.prelude):
if token.type == "hash":
selector.id = selector.id or token.value
elif token.type == "literal" and token.value == ".":
selector.classes.append(style.prelude[count + 1].value)
elif token.type == "[] block" and len(token.content) == 1:
selector.attributes.append(
{
"key": token.content[0].value,
"operator": None,
"value": None,
}
)
elif token.type == "[] block" and len(token.content) == 3:
selector.attributes.append(
{
"key": token.content[0].value,
"operator": token.content[1].value,
"value": token.content[2].value,
}
)
# TODO: Once we reach an operator or whitespace, recursively parse the children then break
# TODO: Split comma operators into separate selectors
self.selectors.append(selector)
return self.selectors
@staticmethod
def _flatten_whitespace(string: str) -> str:
return re.sub(r"\s+", " ", string).strip()
def _parse_rule_value(self, rule):
"""Parses a single TinyCSS rule and returns Python a data type."""
rule = tinycss2.serialize(rule)
rule = self._flatten_whitespace(rule)
if rule.isnumeric():
return int(rule)
else:
return rule
styles = CssToPython(file_text).convert()
for style in styles:
print(style)