.
675 |
676 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include css-html-js-minify.py
2 | recursive-include css-html-js-minify *.py
3 | recursive-include css-html-js-minify *.pxd
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # css-html-js-minify
2 |
3 | Async single-file cross-platform no-dependencies Minifier for the Web. [](http://opensource.org/licenses/GPL-3.0) [](http://opensource.org/licenses/LGPL-3.0) [](http://python.org) [](https://travis-ci.org/juancarlospaco/css-html-js-minify)
4 |
5 | 
6 |
7 |
8 | https://pypi.python.org/pypi/css-html-js-minify
9 |
10 | ```shell
11 | css-html-js-minify.py --help
12 |
13 | usage: css-html-js-minify.py [-h] [--version] [--wrap] [--prefix PREFIX]
14 | [--timestamp] [--quiet] [--hash] [--zipy]
15 | [--sort] [--comments] [--overwrite]
16 | [--after AFTER] [--before BEFORE] [--watch]
17 | [--multiple] [--beep]
18 | fullpath
19 |
20 | CSS-HTML-JS-Minify. StandAlone Async cross-platform Unicode-ready Python3-ready Minifier for the Web.
21 |
22 | positional arguments:
23 | fullpath Full path to local file or folder.
24 |
25 | optional arguments:
26 | -h, --help show this help message and exit
27 | --version show programs version number and exit
28 | --wrap Wrap output to ~80 chars per line, CSS only.
29 | --prefix PREFIX Prefix string to prepend on output filenames.
30 | --timestamp Add a Time Stamp on all CSS/JS output files.
31 | --quiet Quiet, Silent, force disable all logging.
32 | --hash Add SHA1 HEX-Digest 11chars Hash to Filenames.
33 | --zipy GZIP Minified files as '*.gz', CSS/JS only.
34 | --sort Alphabetically Sort CSS Properties, CSS only.
35 | --comments Keep comments, CSS/HTML only (Not Recommended)
36 | --overwrite Force overwrite all in-place (Not Recommended)
37 | --after AFTER Command to execute after run (Experimental).
38 | --before BEFORE Command to execute before run (Experimental).
39 | --watch Re-Compress if file changes (Experimental).
40 | --multiple Allow Multiple instances (Not Recommended).
41 |
42 | CSS-HTML-JS-Minify: Takes a file or folder full path string and process all
43 | CSS/HTML/JS found. If argument is not file/folder will fail. Check Updates
44 | works on Python3. Std-In to Std-Out is deprecated since it may fail with
45 | unicode characters. SHA1 HEX-Digest 11 Chars Hash on Filenames is used for
46 | Server Cache. CSS Properties are Alpha-Sorted, to help spot cloned ones,
47 | Selectors not. Watch works for whole folders, with minimum of ~60 Secs between
48 | runs.
49 |
50 | ```
51 |
52 | - Takes a full path to anything, a file or a folder, then parse, optimize and compress for Production.
53 | - If full path is a folder with multiple files it will use Async Multiprocessing.
54 | - Pretty-Printed colored Logging to Standard Output and Log File on OS Temporary Folder.
55 | - Set its own Process name and show up on Process lists.
56 | - Can check for updates for itself.
57 | - Full Unicode/UTF-8 support.
58 | - Smooth CPU usage, Single Instance Checking.
59 | - Can Obfuscate, GZIP and Hash files, also Watch for changes on files.
60 | - Can execute arbitrary commands after and before running.
61 | - `*.css` files are saved as `*.min.css`, `*.js` are saved as `*.min.js`, `*.htm` are saved as `*.html`
62 |
63 |
64 | # Screenshots
65 |
66 | **Linux:**
67 |
68 | 
69 |
70 | **Apple Mac Os X:**
71 | [ *(Provided by Loggerhead)* ](https://github.com/juancarlospaco/css-html-js-minify/issues/7#issuecomment-97280835)
72 | 
73 |
74 | 
75 |
76 | **MS Windows:**
77 |
78 | 
79 |
80 |
81 | # Command-line usage
82 |
83 | ```bash
84 | css-html-js-minify.py file.htm
85 |
86 | css-html-js-minify.py file.css
87 |
88 | css-html-js-minify.py file.js
89 |
90 | css-html-js-minify.py /project/static/
91 | ```
92 |
93 | # Python code usage
94 |
95 | ```python
96 | from css_html_js_minify import process_single_html_file, process_single_js_file, process_single_css_file, html_minify, js_minify, css_minify
97 |
98 | process_single_html_file('test.htm', overwrite=False)
99 | # 'test.html'
100 | process_single_js_file('test.js', overwrite=False)
101 | # 'test.min.js'
102 | process_single_css_file('test.css', overwrite=False)
103 | # 'test.min.css'
104 |
105 | html_minify(' yoloo
')
106 | # 'yoloo
'
107 | js_minify('var i = 1; i += 2 ;\n alert( "hello " ); //hi')
108 | # 'var i=1;i+=2;alert("hello ");'
109 | css_minify('body {width: 50px;}\np {margin-top: 1em;/* hi */ }', comments=False)
110 | # '@charset utf-8;body{width:50px}p{margin-top:1em}'
111 | ```
112 |
113 | The optional arguments that these functions take are almost the same as the command-line flags.
114 | Check the list above *(just use add_hash instead of hash)*. Additionally, you can force a specific path for the output files using ``output_path``.
115 |
116 |
117 | # Install
118 |
119 | ```
120 | pip install css-html-js-minify
121 | ```
122 | Uninstall `pip uninstall css-html-js-minify`
123 |
124 |
125 | # Why?
126 |
127 | - **Why another Compressor ?**, there are lots of compressors for web files out there!; *Or maybe not ?*.
128 | - Many of them only work inside Django/Flask, or frameworks of PHP/Java/Ruby, or can not process whole folders.
129 | - This project is the big brother of another project that does the inverse, a [Beautifier for the Web.](https://github.com/juancarlospaco/css-html-prettify#css-html-prettify)
130 |
131 |
132 | # Migration
133 |
134 | To keep things simple [KISS](http://en.wikipedia.org/wiki/KISS_principle), the human readable indented commented hackable HTML is kept as `*.htm` and the compressed production-ready as `*.html`. This is inspired from JavaScript/CSS `*.min.js` and `*.min.css`. [We did not "invent" this file extension.](http://en.wikipedia.org/wiki/HTM)
135 |
136 | To migrate from typical file extension HTML to HTM, which is the exactly same, you can run this:
137 |
138 | ```shell
139 | find . -name "*.html" -exec rename "s/.html/.htm/" "{}" \;
140 | ```
141 |
142 | This will make a copy of all `*.html` renaming them as `*.htm` recursively from the current folder. Nothing deleted.
143 |
144 |
145 | # Requisites
146 |
147 | - [Python 3.6+](https://www.python.org "Python Homepage")
148 |
149 |
150 | # Coding Style Guide
151 |
152 | - Lint, [PEP-8](https://www.python.org/dev/peps/pep-0008), [PEP-257](https://www.python.org/dev/peps/pep-0257), [iSort](https://github.com/timothycrosley/isort) must Pass Ok. `pip install pep8 isort`
153 | - If there are any kind of tests, they must pass. No tests is also acceptable, but having tests is better.
154 |
155 |
156 | # JavaScript support
157 |
158 | - ES6 and ES7 and future standards may not be fully supported since they change quickly, mainly driven by Node.JS releases.
159 | - Future JavaScript support is orphan, if you want to make ES6, ES7 work feel free to send pull request, we will merge it.
160 |
161 |
162 | # Contributors
163 |
164 | - **Please Star this Repo on Github !**, it helps to show up faster on searchs.
165 | - [Help](https://help.github.com/articles/using-pull-requests) and more [Help](https://help.github.com/articles/fork-a-repo) and Interactive Quick [Git Tutorial](https://try.github.io).
166 |
167 |
168 | # Licence
169 |
170 | - GNU GPL and GNU LGPL and [MIT](https://github.com/juancarlospaco/css-html-js-minify/issues/65#issuecomment-330983569).
171 |
172 | This work is free software:
173 | You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
174 | This work is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
175 | Without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
176 | See the GNU General Public License for more details.
177 | You should have received a copy of the GNU General Public License along with this work.
178 |
179 |
180 | # Example
181 |
182 |
183 |
184 | **Input CSS:**
185 |
186 | ```css
187 | /*!
188 | * preserve commment
189 | */
190 |
191 |
192 | /* delete comment */
193 | .class, #NotHex, input[type="text"], a:hover {
194 | font-family : Helvetica Neue, Arial, Helvetica, 'Liberation Sans', sans-serif;
195 | border: none;
196 | margin: 0 0 0 0;
197 | border-color: fuchsia;
198 | color: mediumspringgreen;
199 | background-position:0 0;;
200 | transform-origin:0 0;
201 | margin: 0px !important;
202 | font-weight :bold;
203 | color: rgb( 255, 255, 255 );
204 | padding : 0.9px;
205 | position : absolute;
206 | z-index : 100000;
207 | color: #000000;
208 | background-color: #FFFFFF;
209 | background-image: url("data:image/jpeg;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=");
210 | ;}
211 | ;;
212 |
213 | ```
214 |
215 | **Uglify (NodeJS):** *(474 Bytes, 0.189 Secs)*
216 |
217 | ```css
218 | /* * preserve commment */ .class,#NotHex,input[type="text"],a:hover {font-family:Helvetica Neue,Arial,Helvetica,'Liberation Sans',sans-serif;border:0;margin:0;border-color:fuchsia;color:mediumspringgreen;background-position:0 0;transform-origin:0 0;margin:0 !important;font-weight:bold;color:#fff;padding:.9px;position:absolute;z-index:100000;color:#000;background-color:#fff;background-image:url("data:image/jpeg;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=")};
219 | ```
220 |
221 | **css-html-js-minify (Python3):** *(469 Bytes, 0.010 Secs)*
222 |
223 | ```css
224 | /*!* preserve commment */ .class,#NotHex,input[type=text],a:hover{font-family:Helvetica Neue,Arial,Helvetica,'Liberation Sans',sans-serif;border:0;margin:0;border-color:#f0f;color:#00fa9a;background-position:0 0;transform-origin:0 0;margin:0 !important;font-weight:700;color:#fff;padding:.9px;position:absolute;z-index:100000;color:#000;background-color:#FFF;background-image:url(data:image/jpg;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=)}
225 | ```
226 |
227 |
228 |
229 |
230 | # Ethics and Humanism Policy
231 |
232 | - May this FLOSS be always Pristine and Clean, No AdWare, No Spamm, No BundleWare, No Infomercial, No MalWare.
233 | - This project is [LGBTQQIAAP friendly](http://www.urbandictionary.com/define.php?term=LGBTQQIAAP "Whats LGBTQQIAAP").
234 |
--------------------------------------------------------------------------------
/__main__.py:
--------------------------------------------------------------------------------
1 | css-html-js-minify.py
--------------------------------------------------------------------------------
/code_of_conduct.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team.
59 | All complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/css-html-js-minify.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | """CSS-HTML-JS-Minify.
6 |
7 | StandAlone Async cross-platform no-dependency Minifier for the Web.
8 | """
9 |
10 |
11 | from css_html_js_minify.minify import main
12 |
13 |
14 | if __name__ in '__main__':
15 | main()
16 |
--------------------------------------------------------------------------------
/css_html_js_minify/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | """CSS-HTML-JS-Minify.
6 |
7 | Minifier for the Web.
8 | """
9 |
10 |
11 | from .minify import (process_single_html_file, process_single_js_file,
12 | process_single_css_file, html_minify, js_minify,
13 | css_minify)
14 |
15 |
16 | __version__ = '2.5.5'
17 | __all__ = ('__version__', 'process_single_html_file', 'process_single_js_file',
18 | 'process_single_css_file', 'html_minify', 'js_minify', 'css_minify',
19 | 'minify')
20 |
--------------------------------------------------------------------------------
/css_html_js_minify/css_minifier.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | """CSS Minifier functions for CSS-HTML-JS-Minify."""
6 |
7 |
8 | import re
9 | import itertools
10 |
11 | from .variables import EXTENDED_NAMED_COLORS, CSS_PROPS_TEXT
12 |
13 |
14 | __all__ = ('css_minify', 'condense_semicolons')
15 |
16 |
17 | def _compile_props(props_text, grouped=False):
18 | """Take a list of props and prepare them."""
19 | props, prefixes = [], "-webkit-,-khtml-,-epub-,-moz-,-ms-,-o-,".split(",")
20 | for propline in props_text.strip().lower().splitlines():
21 | props += [pre + pro for pro in propline.split(" ") for pre in prefixes]
22 | props = filter(lambda line: not line.startswith('#'), props)
23 | if not grouped:
24 | props = list(filter(None, props))
25 | return props, [0]*len(props)
26 | final_props, groups, g_id = [], [], 0
27 | for prop in props:
28 | if prop.strip():
29 | final_props.append(prop)
30 | groups.append(g_id)
31 | else:
32 | g_id += 1
33 | return final_props, groups
34 |
35 |
36 | def _prioritify(line_of_css, css_props_text_as_list):
37 | """Return args priority, priority is integer and smaller means higher."""
38 | sorted_css_properties, groups_by_alphabetic_order = css_props_text_as_list
39 | priority_integer, group_integer = 9999, 0
40 | for css_property in sorted_css_properties:
41 | if css_property.lower() == line_of_css.split(":")[0].lower().strip():
42 | priority_integer = sorted_css_properties.index(css_property)
43 | group_integer = groups_by_alphabetic_order[priority_integer]
44 | break
45 | return priority_integer, group_integer
46 |
47 |
48 | def _props_grouper(props, pgs):
49 | """Return groups for properties."""
50 | if not props:
51 | return props
52 | # props = sorted([
53 | # _ if _.strip().endswith(";")
54 | # and not _.strip().endswith("*/") and not _.strip().endswith("/*")
55 | # else _.rstrip() + ";\n" for _ in props])
56 | props_pg = zip(map(lambda prop: _prioritify(prop, pgs), props), props)
57 | props_pg = sorted(props_pg, key=lambda item: item[0][1])
58 | props_by_groups = map(
59 | lambda item: list(item[1]),
60 | itertools.groupby(props_pg, key=lambda item: item[0][1]))
61 | props_by_groups = map(lambda item: sorted(
62 | item, key=lambda item: item[0][0]), props_by_groups)
63 | props = []
64 | for group in props_by_groups:
65 | group = map(lambda item: item[1], group)
66 | props += group
67 | props += ['\n']
68 | props.pop()
69 | return props
70 |
71 |
72 | def sort_properties(css_unsorted_string):
73 | """CSS Property Sorter Function.
74 |
75 | This function will read buffer argument, split it to a list by lines,
76 | sort it by defined rule, and return sorted buffer if it's CSS property.
77 | This function depends on '_prioritify' function.
78 | """
79 | css_pgs = _compile_props(CSS_PROPS_TEXT, grouped=False) # Do Not Group.
80 | pattern = re.compile(r'(.*?{\r?\n?)(.*?)(}.*?)|(.*)',
81 | re.DOTALL + re.MULTILINE)
82 | matched_patterns = pattern.findall(css_unsorted_string)
83 | sorted_patterns, sorted_buffer = [], css_unsorted_string
84 | re_prop = re.compile(r'((?:.*?)(?:;)(?:.*?\n)|(?:.*))',
85 | re.DOTALL + re.MULTILINE)
86 | if len(matched_patterns) != 0:
87 | for matched_groups in matched_patterns:
88 | sorted_patterns += matched_groups[0].splitlines(True)
89 | props = map(lambda line: line.lstrip('\n'),
90 | re_prop.findall(matched_groups[1]))
91 | props = list(filter(lambda line: line.strip('\n '), props))
92 | props = _props_grouper(props, css_pgs)
93 | sorted_patterns += props
94 | sorted_patterns += matched_groups[2].splitlines(True)
95 | sorted_patterns += matched_groups[3].splitlines(True)
96 | sorted_buffer = ''.join(sorted_patterns)
97 | return sorted_buffer
98 |
99 |
100 | def remove_comments(css):
101 | """Remove all CSS comment blocks."""
102 | iemac, preserve = False, False
103 | comment_start = css.find("/*")
104 | while comment_start >= 0: # Preserve comments that look like `/*!...*/`.
105 | # Slicing is used to make sure we dont get an IndexError.
106 | preserve = css[comment_start + 2:comment_start + 3] == "!"
107 | comment_end = css.find("*/", comment_start + 2)
108 | if comment_end < 0:
109 | if not preserve:
110 | css = css[:comment_start]
111 | break
112 | elif comment_end >= (comment_start + 2):
113 | if css[comment_end - 1] == "\\":
114 | # This is an IE Mac-specific comment; leave this one and the
115 | # following one alone.
116 | comment_start = comment_end + 2
117 | iemac = True
118 | elif iemac:
119 | comment_start = comment_end + 2
120 | iemac = False
121 | elif not preserve:
122 | css = css[:comment_start] + css[comment_end + 2:]
123 | else:
124 | comment_start = comment_end + 2
125 | comment_start = css.find("/*", comment_start)
126 | return css
127 |
128 |
129 | def remove_unnecessary_whitespace(css):
130 | """Remove unnecessary whitespace characters."""
131 |
132 | def pseudoclasscolon(css):
133 | """Prevent 'p :link' from becoming 'p:link'.
134 |
135 | Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'.
136 | This is translated back again later.
137 | """
138 | regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
139 | match = regex.search(css)
140 | while match:
141 | css = ''.join([
142 | css[:match.start()],
143 | match.group().replace(":", "___PSEUDOCLASSCOLON___"),
144 | css[match.end():]])
145 | match = regex.search(css)
146 | return css
147 |
148 | css = pseudoclasscolon(css)
149 | # Remove spaces from before things.
150 | css = re.sub(r"\s+([!{};:>\(\)\],])", r"\1", css)
151 | # If there is a `@charset`, then only allow one, and move to beginning.
152 | css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
153 | css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
154 | # Put the space back in for a few cases, such as `@media screen` and
155 | # `(-webkit-min-device-pixel-ratio:0)`.
156 | css = re.sub(r"\band\(", "and (", css)
157 | # Put the colons back.
158 | css = css.replace('___PSEUDOCLASSCOLON___', ':')
159 | # Remove spaces from after things.
160 | css = re.sub(r"([!{}:;>\(\[,])\s+", r"\1", css)
161 | return css
162 |
163 |
164 | def remove_unnecessary_semicolons(css):
165 | """Remove unnecessary semicolons."""
166 | return re.sub(r";+\}", "}", css)
167 |
168 |
169 | def remove_empty_rules(css):
170 | """Remove empty rules."""
171 | return re.sub(r"[^\}\{]+\{\}", "", css)
172 |
173 |
174 | def normalize_rgb_colors_to_hex(css):
175 | """Convert `rgb(51,102,153)` to `#336699`."""
176 | regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
177 | match = regex.search(css)
178 | while match:
179 | colors = map(lambda s: s.strip(), match.group(1).split(","))
180 | hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
181 | css = css.replace(match.group(), hexcolor)
182 | match = regex.search(css)
183 | return css
184 |
185 |
186 | def condense_zero_units(css):
187 | """Replace `0(px, em, %, etc)` with `0`."""
188 | return re.sub(r"([\s:])(0)(px|em|%|in|q|ch|cm|mm|pc|pt|ex|rem|s|ms|"
189 | r"deg|grad|rad|turn|vw|vh|vmin|vmax|fr)", r"\1\2", css)
190 |
191 |
192 | def condense_multidimensional_zeros(css):
193 | """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
194 | return css.replace(":0 0 0 0;", ":0;").replace(
195 | ":0 0 0;", ":0;").replace(":0 0;", ":0;").replace(
196 | "background-position:0;", "background-position:0 0;").replace(
197 | "transform-origin:0;", "transform-origin:0 0;")
198 |
199 |
200 | def condense_floating_points(css):
201 | """Replace `0.6` with `.6` where possible."""
202 | return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
203 |
204 |
205 | def condense_hex_colors(css):
206 | """Shorten colors from #AABBCC to #ABC where possible."""
207 | regex = re.compile(
208 | r"""([^\"'=\s])(\s*)#([0-9a-f])([0-9a-f])([0-9a-f])"""
209 | r"""([0-9a-f])([0-9a-f])([0-9a-f])""", re.I | re.S)
210 | match = regex.search(css)
211 | while match:
212 | first = match.group(3) + match.group(5) + match.group(7)
213 | second = match.group(4) + match.group(6) + match.group(8)
214 | if first.lower() == second.lower():
215 | css = css.replace(
216 | match.group(), match.group(1) + match.group(2) + '#' + first)
217 | match = regex.search(css, match.end() - 3)
218 | else:
219 | match = regex.search(css, match.end())
220 | return css
221 |
222 |
223 | def condense_whitespace(css):
224 | """Condense multiple adjacent whitespace characters into one."""
225 | return re.sub(r"\s+", " ", css)
226 |
227 |
228 | def condense_semicolons(css):
229 | """Condense multiple adjacent semicolon characters into one."""
230 | return re.sub(r";;+", ";", css)
231 |
232 |
233 | def wrap_css_lines(css, line_length=80):
234 | """Wrap the lines of the given CSS to an approximate length."""
235 | lines, line_start = [], 0
236 | for i, char in enumerate(css):
237 | # Its safe to break after } characters.
238 | if char == '}' and (i - line_start >= line_length):
239 | lines.append(css[line_start:i + 1])
240 | line_start = i + 1
241 | if line_start < len(css):
242 | lines.append(css[line_start:])
243 | return '\n'.join(lines)
244 |
245 |
246 | def condense_font_weight(css):
247 | """Condense multiple font weights into shorter integer equals."""
248 | return css.replace('font-weight:normal;', 'font-weight:400;').replace(
249 | 'font-weight:bold;', 'font-weight:700;')
250 |
251 |
252 | def condense_std_named_colors(css):
253 | """Condense named color values to shorter replacement using HEX."""
254 | for color_name, color_hexa in iter(tuple({
255 | ':aqua;': ':#0ff;', ':blue;': ':#00f;',
256 | ':fuchsia;': ':#f0f;', ':yellow;': ':#ff0;'}.items())):
257 | css = css.replace(color_name, color_hexa)
258 | return css
259 |
260 |
261 | def condense_xtra_named_colors(css):
262 | """Condense named color values to shorter replacement using HEX."""
263 | for k, v in iter(tuple(EXTENDED_NAMED_COLORS.items())):
264 | same_color_but_rgb = 'rgb({0},{1},{2})'.format(v[0], v[1], v[2])
265 | if len(k) > len(same_color_but_rgb):
266 | css = css.replace(k, same_color_but_rgb)
267 | return css
268 |
269 |
270 | def remove_url_quotes(css):
271 | """Fix for url() does not need quotes."""
272 | return re.sub(r'url\((["\'])([^)]*)\1\)', r'url(\2)', css)
273 |
274 |
275 | def condense_border_none(css):
276 | """Condense border:none; to border:0;."""
277 | return css.replace("border:none;", "border:0;")
278 |
279 |
280 | def add_encoding(css):
281 | """Add @charset 'UTF-8'; if missing."""
282 | return '@charset "utf-8";' + css if "@charset" not in css.lower() else css
283 |
284 |
285 | def restore_needed_space(css):
286 | """Fix CSS for some specific cases where a white space is needed."""
287 | return css.replace("!important", " !important").replace( # !important
288 | "@media(", "@media (").replace( # media queries # jpeg > jpg
289 | "data:image/jpeg;base64,", "data:image/jpg;base64,").rstrip("\n;")
290 |
291 |
292 | def unquote_selectors(css):
293 | """Fix CSS for some specific selectors where Quotes is not needed."""
294 | return re.compile('([a-zA-Z]+)="([a-zA-Z0-9-_\.]+)"]').sub(r'\1=\2]', css)
295 |
296 |
297 | def css_minify(css, wrap=False, comments=False, sort=False, noprefix=False):
298 | """Minify CSS main function."""
299 | css = remove_comments(css) if not comments else css
300 | css = sort_properties(css) if sort else css
301 | css = unquote_selectors(css)
302 | css = condense_whitespace(css)
303 | css = remove_url_quotes(css)
304 | css = condense_xtra_named_colors(css)
305 | css = condense_font_weight(css)
306 | css = remove_unnecessary_whitespace(css)
307 | css = condense_std_named_colors(css)
308 | css = remove_unnecessary_semicolons(css)
309 | css = condense_zero_units(css)
310 | css = condense_multidimensional_zeros(css)
311 | css = condense_floating_points(css)
312 | css = normalize_rgb_colors_to_hex(css)
313 | css = condense_hex_colors(css)
314 | css = condense_border_none(css)
315 | css = wrap_css_lines(css, 80) if wrap else css
316 | css = condense_semicolons(css)
317 | css = add_encoding(css) if not noprefix else css
318 | css = restore_needed_space(css)
319 | return css.strip()
320 |
--------------------------------------------------------------------------------
/css_html_js_minify/html_minifier.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | """HTML Minifier functions for CSS-HTML-JS-Minify."""
6 |
7 |
8 | import re
9 |
10 |
11 | __all__ = ('html_minify', )
12 |
13 |
14 | def condense_html_whitespace(html):
15 | """Condense HTML, but be safe first if it have textareas or pre tags.
16 |
17 | >>> condense_html_whitespace(' test
')
18 | ' test
'
19 | """ # first space between tags, then empty new lines and in-between.
20 | tagsStack = []
21 | split = re.split('(<\\s*pre.*>|<\\s*/\\s*pre\\s*>|<\\s*textarea.*>|<\\s*/\\s*textarea\\s*>)', html, flags=re.IGNORECASE)
22 | for i in range(0, len(split)):
23 | #if we are on a tag
24 | if (i + 1) % 2 == 0:
25 | tag = rawtag(split[i])
26 | if tag.startswith('/'):
27 | if not tagsStack or '/' + tagsStack.pop() != tag:
28 | raise Exception("Some tag is not closed properly")
29 | else:
30 | tagsStack.append(tag)
31 | continue
32 |
33 | #else check if we are outside any nested /