Skip to content

Commit efdbb34

Browse files
committed
add minify css tutorial
1 parent 83548ad commit efdbb34

File tree

7 files changed

+362
-0
lines changed

7 files changed

+362
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ This is a repository of all the tutorials of [The Python Code](https://www.thepy
121121
- [How to Generate SVG Country Maps in Python](https://www.thepythoncode.com/article/generate-svg-country-maps-python). ([code](general/generate-svg-country-map))
122122
- [How to Query the Ethereum Blockchain with Python](https://www.thepythoncode.com/article/query-ethereum-blockchain-with-python). ([code](general/query-ethereum))
123123
- [Data Cleaning with Pandas in Python](https://www.thepythoncode.com/article/data-cleaning-using-pandas-in-python). ([code](general/data-cleaning-pandas))
124+
- [How to Minify CSS with Python](https://www.thepythoncode.com/article/minimize-css-files-in-python). ([code](general/minify-css))
124125

125126

126127

general/minify-css/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# [How to Minify CSS with Python](https://www.thepythoncode.com/article/minimize-css-files-in-python)
2+
To run this:
3+
- `pip install -r requirements.txt`.
4+
- Put your CSS file in the `style` folder.
5+
- Put your HTML file in the current (root) folder.
6+
- Run `python minimize.py`
7+
- A new file will appear named `min.css` in the current working folder.

general/minify-css/index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Document</title>
8+
</head>
9+
<body>
10+
<div class="page">
11+
<div class="article">
12+
13+
</div>
14+
<div class="article">
15+
16+
</div>
17+
</div>
18+
</body>
19+
</html>

general/minify-css/minimize.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import cssutils
2+
import re
3+
import logging
4+
import os
5+
import time
6+
cssutils.log.setLevel(logging.CRITICAL)
7+
8+
startTime = time.time()
9+
os.system('cls')
10+
11+
def getFilesByExtension(ext, root):
12+
foundFiles = []
13+
for root, directories, files in os.walk(root):
14+
for f in files:
15+
if f.endswith(ext):
16+
# os.path.join(root, f) is the full path to the file
17+
foundFiles.append(os.path.join(root, f))
18+
return foundFiles
19+
20+
21+
def flattenStyleSheet(sheet):
22+
ruleList = []
23+
for rule in sheet.cssRules:
24+
if rule.typeString == 'MEDIA_RULE':
25+
ruleList += rule.cssRules
26+
elif rule.typeString == 'STYLE_RULE':
27+
ruleList.append(rule)
28+
return ruleList
29+
30+
31+
def findAllCSSClasses():
32+
usedClasses = {}
33+
# Find all used classes
34+
for htmlFile in htmlFiles:
35+
with open(htmlFile, 'r') as f:
36+
htmlContent = f.read()
37+
regex = r'class="(.*?)"'
38+
# re.DOTALL is needed to match newlines
39+
matched = re.finditer(regex, htmlContent, re.MULTILINE | re.DOTALL)
40+
# matched is a list of re.Match objects
41+
for i in matched:
42+
for className in i.groups()[0].split(' '): # i.groups()[0] is the first group in the regex
43+
usedClasses[className] = ''
44+
return list(usedClasses.keys())
45+
46+
47+
def translateUsedClasses(classList):
48+
for i, usedClass in enumerate(classList):
49+
for translation in translations:
50+
# If the class is found in the translations list, replace it
51+
regex = translation[0]
52+
subst = translation[1]
53+
if re.search(regex, usedClass):
54+
# re.sub() replaces the regex with the subst
55+
result = re.sub(regex, subst, usedClass, 1, re.MULTILINE) # 1 is the max number of replacements
56+
# Replace the class in the list
57+
classList[i] = result
58+
return classList
59+
60+
61+
htmlFiles = getFilesByExtension('.html', '.')
62+
63+
cssFiles = getFilesByExtension('.css', 'style')
64+
65+
# Use Translations if the class names in the Markup dont exactly
66+
# match the CSS Selector ( Except for the dot at the begining. )
67+
translations = [
68+
[
69+
'@',
70+
'\\@'
71+
],
72+
[
73+
r"(.*?):(.*)",
74+
r"\g<1>\\:\g<2>:\g<1>",
75+
],
76+
[
77+
r"child(.*)",
78+
"child\\g<1> > *",
79+
],
80+
]
81+
82+
usedClasses = findAllCSSClasses()
83+
usedClasses = translateUsedClasses(usedClasses)
84+
85+
output = 'min.css'
86+
87+
newCSS = ''
88+
89+
for cssFile in cssFiles:
90+
# Parse the CSS File
91+
sheet = cssutils.parseFile(cssFile)
92+
rules = flattenStyleSheet(sheet)
93+
noClassSelectors = []
94+
for rule in rules:
95+
for usedClass in usedClasses:
96+
if '.' + usedClass == rule.selectorText:
97+
# If the class is used in the HTML, add it to the new CSS
98+
usedClasses.remove(usedClass) # Remove the class from the list
99+
if rule.parentRule:
100+
newCSS += str(rule.parentRule.cssText)
101+
else:
102+
newCSS += str(rule.cssText)
103+
if rule.selectorText[0] != '.' and not rule.selectorText in noClassSelectors:
104+
# If the selector doesnt start with a dot and is not already in the list,
105+
# add it
106+
noClassSelectors.append(rule.selectorText)
107+
if rule.parentRule:
108+
newCSS += str(rule.parentRule.cssText)
109+
else:
110+
newCSS += str(rule.cssText)
111+
112+
newCSS = newCSS.replace('\n', '')
113+
newCSS = newCSS.replace(' ', '')
114+
115+
with open(output, 'w') as f:
116+
f.write(newCSS)
117+
118+
119+
print('TIME TOOK: ', time.time() - startTime)

general/minify-css/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cssutils

general/minify-css/style/style.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.page {
2+
color: pink
3+
}
4+
5+
.article {
6+
font-size: 1rem;
7+
}
8+
9+
.button {
10+
padding: 1rem;
11+
}
12+
13+
body {
14+
font-family: 'Lucida Sans', sans-serif;
15+
}

general/minify-css/text.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Minify CSS with Python
2+
**Learn how to utilize cssutils to minimize CSS files in a Web Project**
3+
4+
5+
## Idea
6+
7+
In this article, we will make a python program that will search for classes used in all HTML files in a project and it will then search and compile these files from the CSS files. The program will serve a specific purpose as it will match classes strictly; which means `bg-black` won't `bg-black:hover`, The used classes have to appear in the stylesheets as they are used. This way of minimizing is useful for utility classes such as `width-800px` or `color-grey-800` that only change on the property. Now maybe your utility classes also entail something like this pattern: `child-margin-2rem` which in the stylesheet is actually `child-margin-2rem > *`, this won't match by default but we will make it possible to replace such patterns with the appropriate selector. Finally, you can change the code so the minified works better for your case or you could even redo it on your own with the knowledge gained.
8+
9+
We will utilize a CSS Library called CSSUtils that allows us to parse, read and write CSS.
10+
11+
## Imports
12+
13+
Let's start with the Modules and Libraries we have to import for our little program. The most important will be `cssutils` which has to be downloaded with `pip install cssutils`. We also want to import `re`, `os`, `time`. We get the logging module simply to turn off logging because cssutils throws a lot of errors. We then clear the console with `os.system` and we save the start time of the program to a variable.
14+
15+
```py
16+
import cssutils
17+
import re
18+
import logging
19+
import os
20+
import time
21+
cssutils.log.setLevel(logging.CRITICAL)
22+
23+
startTime = time.time()
24+
os.system('cls')
25+
```
26+
27+
## Getting the Files
28+
29+
Firstly we get lists of files ending in `.html` and `.css`. We save these lists for later.
30+
31+
```py
32+
htmlFiles = getFilesByExtension('.html', '.')
33+
34+
cssFiles = getFilesByExtension('.css', 'style')
35+
```
36+
37+
Let's also go over the function that searches for all these files. keep in mind it has to be defined before its usage. Here we use the `walk` function of `os` which receives a path and it will return data about each subdirectory and the directory itself. We only need the files which are the third item of the returned tuple. We loop over these and if they end with the specified extension we add them to the `foundFiles` list. Lastly, we also need to return this list.
38+
39+
```py
40+
def getFilesByExtension(ext, root):
41+
foundFiles = []
42+
43+
for root, directories, files in os.walk(root):
44+
45+
for f in files:
46+
47+
if f.endswith(ext):
48+
foundFiles.append(os.path.join(root, f))
49+
```
50+
51+
## Finding all used Classes
52+
53+
Next up we want to find all used classes in all HTML files that were found. To do this we first create a dictionary to store each class name as an item. We do it this way so we don't have duplicates in the end. We then loop over all HTML files and for each one we get the content and we use a Regular Expression to find all class strings. Continuing we split each of these found strings because classes are separated by a space. Lastly, we return the found list dictionary but we return the keys which are the classes.
54+
55+
```py
56+
usedClasses = findAllCSSClasses()
57+
58+
# Function, defined before
59+
def findAllCSSClasses():
60+
usedClasses = {}
61+
62+
# Find all used classes
63+
for htmlFile in htmlFiles:
64+
with open(htmlFile, 'r') as f:
65+
htmlContent = f.read()
66+
67+
regex = r'class="(.*?)"'
68+
69+
matched = re.finditer(regex, htmlContent, re.MULTILINE | re.DOTALL)
70+
71+
for i in matched:
72+
73+
for className in i.groups()[0].split(' '):
74+
usedClasses[className] = ''
75+
76+
return list(usedClasses.keys())
77+
```
78+
79+
## Translating used Classes
80+
81+
Now wer translate some classes, this is useful if the class name won't exactly match the selector, but it follows a pattern like all classes starting with `child-` have `> *` appended to their selector, and here we handle this. We define each translation in a list where the first item is the regex and the second is the replacement.
82+
83+
```py
84+
# Use Translations if the class names in the Markup don't exactly
85+
# match the CSS Selector ( Except for the dot at the beginning. )
86+
translations = [
87+
[
88+
'@',
89+
'\\@'
90+
],
91+
[
92+
r"(.*?):(.*)",
93+
r"\g<1>\\:\g<2>:\g<1>",
94+
],
95+
[
96+
r"child(.*)",
97+
"child\\g<1> > *",
98+
],
99+
]
100+
101+
usedClasses = translateUsedClasses(usedClasses)
102+
```
103+
104+
In the function we then loop over each regex for each class, so every translation is potentially applied to each class name. We then simply apply the replacement with the `re.sub` method.
105+
106+
```py
107+
def translateUsedClasses(classList):
108+
109+
for i, usedClass in enumerate(classList):
110+
for translation in translations:
111+
112+
regex = translation[0]
113+
subst = translation[1]
114+
115+
if re.search(regex, usedClass):
116+
result = re.sub(regex, subst, usedClass, 1, re.MULTILINE)
117+
118+
classList[i] = result
119+
120+
return classList
121+
```
122+
123+
## Getting used Classes from the Stylesheets
124+
125+
After that, we get the style definition from the stylesheets with cssutils, before we loop over the found style sheets we first define the path of the minified CSS which in this case is `min.css` then we also create a variable called `newCSS` that will hold the new CSS content.
126+
127+
```py
128+
output = 'min.css'
129+
130+
newCSS = ''
131+
```
132+
133+
We continue by looping over all CSS files. We parse each file with `cssutils.parsefile(path)` and get all the rules in the style sheet with the custom `flattenStyleSheet()` function, we later go over how it works but it will essentially put all rules hidden inside media queries into the same list as top-level rules. then we define a list that will hold all selector names that are not classes that we encounter. We do this because something like `input` should not be left out. Then we loop over each rule and each class and if the selector and selector text of the rule match up we add the whole CSS text of the rule to the newCSS string. We simply need to watch out if the rule has a parent rule which would be a media query. We do the same thing for all the rules not starting with a dot.
134+
135+
```py
136+
for cssFile in cssFiles:
137+
138+
sheet = cssutils.parseFile(cssFile)
139+
rules = flattenStyleSheet(sheet)
140+
141+
noClassSelectors = []
142+
143+
for rule in rules:
144+
for usedClass in usedClasses:
145+
146+
if '.' + usedClass == rule.selectorText:
147+
usedClasses.remove(usedClass)
148+
149+
if rule.parentRule:
150+
newCSS += str(rule.parentRule.cssText)
151+
else:
152+
newCSS += str(rule.cssText)
153+
154+
if rule.selectorText[0] != '.' and not rule.selectorText in noClassSelectors:
155+
156+
noClassSelectors.append(rule.selectorText)
157+
158+
if rule.parentRule:
159+
newCSS += str(rule.parentRule.cssText)
160+
else:
161+
newCSS += str(rule.cssText)
162+
```
163+
164+
### `flattenstylesheet` function
165+
166+
Lets quickly go over the flattenstylesheet function. It will receive the sheet and it loops over each rule in that sheet in, then it will check if the rule is simply a style rule or media rule so it can add all rules to a one-dimensional list.
167+
168+
```py
169+
def flattenStyleSheet(sheet):
170+
ruleList = []
171+
172+
for rule in sheet.cssRules:
173+
174+
if rule.typeString == 'MEDIA_RULE':
175+
ruleList += rule.cssRules
176+
177+
elif rule.typeString == 'STYLE_RULE':
178+
ruleList.append(rule)
179+
180+
return ruleList
181+
```
182+
183+
## Saving new CSS
184+
185+
Lastly, we minify the CSS further by removing linebreaks and double spaces and we save this new CSS to the specified location.
186+
187+
```py
188+
newCSS = newCSS.replace('\n', '')
189+
newCSS = newCSS.replace(' ', '')
190+
191+
with open(output, 'w') as f:
192+
f.write(newCSS)
193+
194+
195+
print('TIME: ', time.time() - startTime)
196+
```
197+
198+
## Conclusion
199+
200+
Excellent! You have successfully created a CSS Minifier using Python code! See how you can add more features to this program such as a config file for further options. Also keep in mind that this program could need some optimization since it runs very slow on larger projects.

0 commit comments

Comments
 (0)