59 |
60 |
61 | premailer does this. It parses an HTML page, looks up ``style`` blocks
62 | and parses the CSS. It then uses the ``lxml.html`` parser to modify the
63 | DOM tree of the page accordingly.
64 |
65 | Warning!
66 | By default, premailer will attempt to download any external stylesheets by URL over the Internet.
67 | If you want to prevent this you can use the ``allow_network=False`` option.
68 |
69 | Getting started
70 | ---------------
71 |
72 | If you haven't already done so, install ``premailer`` first:
73 |
74 | ::
75 |
76 | $ pip install premailer
77 |
78 | Next, the most basic use is to use the shortcut function, like this:
79 |
80 | .. code:: python
81 |
82 | >>> from premailer import transform
83 | >>> print(transform("""
84 | ...
85 | ...
90 | ...
93 | ...
107 |
108 |
109 |
110 | The ``transform`` shortcut function transforms the given HTML using the defaults for all options:
111 |
112 | .. code:: python
113 |
114 | base_url=None, # Optional URL prepended to all relative links (both stylesheets and internal)
115 | disable_link_rewrites=False, # Allow link rewrites (e.g. using base_url)
116 | preserve_internal_links=False, # Do not preserve links to named anchors when using base_url
117 | preserve_inline_attachments=True, # Preserve links with cid: scheme when base_url is specified
118 | preserve_handlebar_syntax=False # Preserve handlebar syntax from being encoded
119 | exclude_pseudoclasses=True, # Ignore pseudoclasses when processing styles
120 | keep_style_tags=False, # Discard original style tag
121 | include_star_selectors=False, # Ignore star selectors when processing styles
122 | remove_classes=False, # Leave class attributes on HTML elements
123 | capitalize_float_margin=False, # Do not capitalize float and margin properties
124 | strip_important=True, # Remove !important from property values
125 | external_styles=None, # Optional list of URLs to load and parse
126 | css_text=None, # Optional CSS text to parse
127 | method="html", # Parse input as HTML (as opposed to "xml")
128 | base_path=None, # Optional base path to stylesheet in your file system
129 | disable_basic_attributes=None, # Optional list of attribute names to preserve on HTML elements
130 | disable_validation=False, # Validate CSS when parsing it with cssutils
131 | cache_css_parsing=True, # Do cache parsed output for CSS
132 | cssutils_logging_handler=None, # See "Capturing logging from cssutils" below
133 | cssutils_logging_level=None,
134 | disable_leftover_css=False, # Output CSS that was not inlined into the HEAD
135 | align_floating_images=True, # Add align attribute for floated images
136 | remove_unset_properties=True # Remove CSS properties if their value is unset when merged
137 | allow_network=True # allow network access to fetch linked css files
138 | allow_insecure_ssl=False # Don't allow unverified SSL certificates for external links
139 | allow_loading_external_files=False # Allow loading any non-HTTP external file URL
140 | session=None # Session used for http requests - supply your own for caching or to provide authentication
141 |
142 | For more advanced options, check out the code of the ``Premailer`` class
143 | and all its options in its constructor.
144 |
145 | You can also use premailer from the command line by using its main
146 | module.
147 |
148 | ::
149 |
150 | $ python -m premailer -h
151 | usage: python -m premailer [options]
152 |
153 | optional arguments:
154 | -h, --help show this help message and exit
155 | -f [INFILE], --file [INFILE]
156 | Specifies the input file. The default is stdin.
157 | -o [OUTFILE], --output [OUTFILE]
158 | Specifies the output file. The default is stdout.
159 | --base-url BASE_URL
160 | --remove-internal-links PRESERVE_INTERNAL_LINKS
161 | Remove links that start with a '#' like anchors.
162 | --exclude-pseudoclasses
163 | Pseudo classes like p:last-child', p:first-child, etc
164 | --preserve-style-tags
165 | Do not delete tags from the html
166 | document.
167 | --remove-star-selectors
168 | All wildcard selectors like '* {color: black}' will be
169 | removed.
170 | --remove-classes Remove all class attributes from all elements
171 | --strip-important Remove '!important' for all css declarations.
172 | --method METHOD The type of html to output. 'html' for HTML, 'xml' for
173 | XHTML.
174 | --base-path BASE_PATH
175 | The base path for all external stylsheets.
176 | --external-style EXTERNAL_STYLES
177 | The path to an external stylesheet to be loaded.
178 | --disable-basic-attributes DISABLE_BASIC_ATTRIBUTES
179 | Disable provided basic attributes (comma separated)
180 | --disable-validation Disable CSSParser validation of attributes and values
181 | --pretty Pretty-print the outputted HTML.
182 | --allow-insecure-ssl Skip SSL certificate verification for external URLs.
183 | --allow-loading-external-files Allow opening any non-HTTP external file URL.
184 |
185 | A basic example:
186 |
187 | ::
188 |
189 | $ python -m premailer --base-url=http://google.com/ -f newsletter.html
190 |
191 |
192 |
203 |
204 |
205 | Turning relative URLs into absolute URLs
206 | ----------------------------------------
207 |
208 | Another thing premailer can do for you is to turn relative URLs (e.g.
209 | "/some/page.html" into "http://www.peterbe.com/some/page.html"). It does
210 | this to all ``href`` and ``src`` attributes that don't have a ``://``
211 | part in it. For example, turning this:
212 |
213 | .. code:: html
214 |
215 |
216 |
217 | Home
218 | Page
219 | External
220 | Folder
221 |
222 |
223 |
224 | Into this:
225 |
226 | .. code:: html
227 |
228 |
229 |
230 | Home
231 | Page
232 | External
233 | Folder
234 |
235 |
236 |
237 | by using ``transform('...', base_url='http://www.peterbe.com/')``.
238 |
239 | Ignore certain ``
249 |
250 |
251 |
252 | That tag gets completely ignored except when the HTML is processed, the
253 | attribute ``data-premailer`` is removed.
254 |
255 | It works equally for a ```` tag like:
256 |
257 | .. code:: html
258 |
259 |
260 |
261 |
262 |
263 | HTML attributes created additionally
264 | ------------------------------------
265 |
266 | Certain HTML attributes are also created on the HTML if the CSS contains
267 | any ones that are easily translated into HTML attributes. For example,
268 | if you have this CSS: ``td { background-color:#eee; }`` then this is
269 | transformed into ``style="background-color:#eee"`` and as an HTML
270 | attribute ``bgcolor="#eee"``.
271 |
272 | Having these extra attributes basically as a "back up" for really shit
273 | email clients that can't even take the style attributes. A lot of
274 | professional HTML newsletters such as Amazon's use this. You can disable
275 | some attributes in ``disable_basic_attributes``.
276 |
277 |
278 | Capturing logging from ``cssutils``
279 | -----------------------------------
280 |
281 | `cssutils `__ is the library that
282 | ``premailer`` uses to parse CSS. It will use the python ``logging`` module
283 | to mention all issues it has with parsing your CSS. If you want to capture
284 | this, you have to pass in ``cssutils_logging_handler`` and
285 | ``cssutils_logging_level`` (optional). For example like this:
286 |
287 | .. code:: python
288 |
289 | >>> import logging
290 | >>> import premailer
291 | >>> from io import StringIO
292 | >>> mylog = StringIO()
293 | >>> myhandler = logging.StreamHandler(mylog)
294 | >>> p = premailer.Premailer(
295 | ... cssutils_logging_handler=myhandler,
296 | ... cssutils_logging_level=logging.INFO
297 | ... )
298 | >>> result = p.transform("""
299 | ...
300 | ...
303 | ...
Hej
304 | ...
305 | ... """)
306 | >>> mylog.getvalue()
307 | 'CSSStylesheet: Unknown @rule found. [2:1: @keyframes]\n'
308 |
309 |
310 | If execution speed is on your mind
311 | ----------------------------------
312 |
313 | If execution speed is important, it's very plausible that you're not just converting
314 | 1 HTML document but *a lot* of HTML documents. Then, the first thing you should do
315 | is avoid using the ``premailer.transform`` function because it creates a ``Premailer``
316 | class instance every time.
317 |
318 | .. code:: python
319 |
320 | # WRONG WAY!
321 | from premailer import transform
322 |
323 | for html_string in get_html_documents():
324 | transformed = transform(html_string, base_url=MY_BASE_URL)
325 | # do something with 'transformed'
326 |
327 | Instead...
328 |
329 | .. code:: python
330 |
331 | # RIGHT WAY
332 | from premailer import Premailer
333 |
334 | instance = Premailer(base_url=MY_BASE_URL)
335 | for html_string in get_html_documents():
336 | transformed = instance.transform(html_string)
337 | # do something with 'transformed'
338 |
339 | Another thing to watch out for when you're reusing the same imported Python code
340 | and reusing it is that internal memoize function caches might build up. The
341 | environment variable to control is ``PREMAILER_CACHE_MAXSIZE``. This parameter
342 | requires a little bit of fine-tuning and calibration if your workload is really
343 | big and memory even becomes an issue.
344 |
345 | Advanced options
346 | ----------------
347 |
348 | Below are some advanced configuration options that probably doesn't matter for
349 | most people with regular load.
350 |
351 | Choosing the cache implementation
352 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
353 |
354 | By default, ``premailer`` uses `LFUCache
355 | `__ to cache
356 | selectors, styles and parsed CSS strings. If LFU doesn't serve your purpose, it
357 | is possible to switch to an alternate implementation using below environment
358 | variables.
359 |
360 | - ``PREMAILER_CACHE``: Can be LRU, LFU or TTL. Default is LFU.
361 | - ``PREMAILER_CACHE_MAXSIZE``: Maximum no. of items to be stored in cache. Defaults to 128.
362 | - ``PREMAILER_CACHE_TTL``: Time to live for cache entries. Only applicable for TTL cache. Defaults to 1 hour.
363 |
364 |
365 | Getting coding
366 | --------------
367 |
368 | First clone the code and create whatever virtualenv you need, then run:
369 |
370 | .. code:: bash
371 |
372 | pip install -e ".[dev]"
373 |
374 |
375 | Then to run the tests, run:
376 |
377 | .. code:: bash
378 |
379 | tox
380 |
381 | This will run the *whole test suite* for every possible version of Python
382 | it can find on your system. To run the tests more incrementally, open
383 | up the ``tox.ini`` and see how it works.
384 |
385 | Code style is all black
386 | -----------------------
387 |
388 | All code has to be formatted with `Black `_
389 | and the best tool for checking this is
390 | `therapist `_ since it can help you run
391 | all, help you fix things, and help you make sure linting is passing before
392 | you git commit. This project also uses ``flake8`` to check other things
393 | Black can't check.
394 |
395 | To check linting with ``tox`` use:
396 |
397 | .. code:: bash
398 |
399 | tox -e lint
400 |
401 | To install the ``therapist`` pre-commit hook simply run:
402 |
403 | .. code:: bash
404 |
405 | therapist install
406 |
407 | When you run ``therapist run`` it will only check the files you've touched.
408 | To run it for all files use:
409 |
410 | .. code:: bash
411 |
412 | therapist run --use-tracked-files
413 |
414 | And to fix all/any issues run:
415 |
416 | .. code:: bash
417 |
418 | therapist run --use-tracked-files --fix
419 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eo pipefail
3 |
4 | # From https://pypi.org/project/twine/
5 |
6 | rm -fr dist/
7 | python setup.py sdist bdist_wheel
8 | twine upload dist/*
9 |
--------------------------------------------------------------------------------
/premailer/__init__.py:
--------------------------------------------------------------------------------
1 | from .premailer import Premailer, transform # noqa
2 |
3 | __version__ = "3.10.0"
4 |
--------------------------------------------------------------------------------
/premailer/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import argparse
3 |
4 | from .premailer import Premailer
5 |
6 |
7 | def main(args):
8 | """Command-line tool to transform html style to inline css
9 |
10 | Usage::
11 |
12 | $ echo '
Title
' | \
13 | python -m premailer
14 |
15 | $ cat newsletter.html | python -m premailer
16 | """
17 |
18 | parser = argparse.ArgumentParser(usage="python -m premailer [options]")
19 |
20 | parser.add_argument(
21 | "-f",
22 | "--file",
23 | nargs="?",
24 | type=argparse.FileType("r"),
25 | help="Specifies the input file. The default is stdin.",
26 | default=sys.stdin,
27 | dest="infile",
28 | )
29 |
30 | parser.add_argument(
31 | "-o",
32 | "--output",
33 | nargs="?",
34 | type=argparse.FileType("w"),
35 | help="Specifies the output file. The default is stdout.",
36 | default=sys.stdout,
37 | dest="outfile",
38 | )
39 |
40 | parser.add_argument("--base-url", default=None, type=str, dest="base_url")
41 |
42 | parser.add_argument(
43 | "--remove-internal-links",
44 | default=True,
45 | help="Remove links that start with a '#' like anchors.",
46 | dest="preserve_internal_links",
47 | )
48 |
49 | parser.add_argument(
50 | "--exclude-pseudoclasses",
51 | default=False,
52 | help="Pseudo classes like p:last-child', p:first-child, etc",
53 | action="store_true",
54 | dest="exclude_pseudoclasses",
55 | )
56 |
57 | parser.add_argument(
58 | "--preserve-style-tags",
59 | default=False,
60 | help="Do not delete tags from the html document.",
61 | action="store_true",
62 | dest="keep_style_tags",
63 | )
64 |
65 | parser.add_argument(
66 | "--remove-star-selectors",
67 | default=True,
68 | help="All wildcard selectors like '* {color: black}' will be removed.",
69 | action="store_false",
70 | dest="include_star_selectors",
71 | )
72 |
73 | parser.add_argument(
74 | "--remove-classes",
75 | default=False,
76 | help="Remove all class attributes from all elements",
77 | action="store_true",
78 | dest="remove_classes",
79 | )
80 |
81 | parser.add_argument(
82 | "--capitalize-float-margin",
83 | default=False,
84 | help="Capitalize float and margin properties for outlook.com compat.",
85 | action="store_true",
86 | dest="capitalize_float_margin",
87 | )
88 |
89 | parser.add_argument(
90 | "--strip-important",
91 | default=False,
92 | help="Remove '!important' for all css declarations.",
93 | action="store_true",
94 | dest="strip_important",
95 | )
96 |
97 | parser.add_argument(
98 | "--method",
99 | default="html",
100 | dest="method",
101 | help="The type of html to output. 'html' for HTML, 'xml' for XHTML.",
102 | )
103 |
104 | parser.add_argument(
105 | "--base-path",
106 | default=None,
107 | dest="base_path",
108 | help="The base path for all external stylsheets.",
109 | )
110 |
111 | parser.add_argument(
112 | "--external-style",
113 | action="append",
114 | dest="external_styles",
115 | help="The path to an external stylesheet to be loaded.",
116 | )
117 |
118 | parser.add_argument(
119 | "--css-text",
120 | action="append",
121 | dest="css_text",
122 | help="CSS text to be applied to the html.",
123 | )
124 |
125 | parser.add_argument(
126 | "--disable-basic-attributes",
127 | dest="disable_basic_attributes",
128 | help="Disable provided basic attributes (comma separated)",
129 | default=[],
130 | )
131 |
132 | parser.add_argument(
133 | "--disable-validation",
134 | default=False,
135 | action="store_true",
136 | dest="disable_validation",
137 | help="Disable CSSParser validation of attributes and values",
138 | )
139 |
140 | parser.add_argument(
141 | "--pretty",
142 | default=False,
143 | action="store_true",
144 | help="Pretty-print the outputted HTML.",
145 | )
146 |
147 | parser.add_argument(
148 | "--encoding", default="utf-8", help="Output encoding. The default is utf-8"
149 | )
150 |
151 | parser.add_argument(
152 | "--allow-insecure-ssl",
153 | default=False,
154 | action="store_true",
155 | help="Skip SSL certificate verification for external URLs.",
156 | )
157 |
158 | parser.add_argument(
159 | "--allow-loading-external-files",
160 | default=False,
161 | action="store_true",
162 | help="Allow opening any non-HTTP external file URL.",
163 | )
164 |
165 | options = parser.parse_args(args)
166 |
167 | if options.disable_basic_attributes:
168 | options.disable_basic_attributes = options.disable_basic_attributes.split()
169 |
170 | html = options.infile.read()
171 | if hasattr(html, "decode"): # Forgive me: Python 2 compatability
172 | html = html.decode("utf-8")
173 |
174 | p = Premailer(
175 | html=html,
176 | base_url=options.base_url,
177 | preserve_internal_links=options.preserve_internal_links,
178 | exclude_pseudoclasses=options.exclude_pseudoclasses,
179 | keep_style_tags=options.keep_style_tags,
180 | include_star_selectors=options.include_star_selectors,
181 | remove_classes=options.remove_classes,
182 | strip_important=options.strip_important,
183 | external_styles=options.external_styles,
184 | css_text=options.css_text,
185 | method=options.method,
186 | base_path=options.base_path,
187 | disable_basic_attributes=options.disable_basic_attributes,
188 | disable_validation=options.disable_validation,
189 | allow_insecure_ssl=options.allow_insecure_ssl,
190 | allow_loading_external_files=options.allow_loading_external_files,
191 | )
192 | options.outfile.write(
193 | p.transform(encoding=options.encoding, pretty_print=options.pretty)
194 | )
195 | return 0
196 |
197 |
198 | if __name__ == "__main__": # pragma: no cover
199 | sys.exit(main(sys.argv[1:]))
200 |
--------------------------------------------------------------------------------
/premailer/cache.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import os
3 | import threading
4 |
5 | import cachetools
6 |
7 |
8 | # Available cache options.
9 | CACHE_IMPLEMENTATIONS = {
10 | "LFU": cachetools.LFUCache,
11 | "LRU": cachetools.LRUCache,
12 | "TTL": cachetools.TTLCache,
13 | }
14 |
15 | # Time to live (seconds) for entries in TTL cache. Defaults to 1 hour.
16 | TTL_CACHE_TIMEOUT = 1 * 60 * 60
17 |
18 | # Maximum no. of items to be saved in cache.
19 | DEFAULT_CACHE_MAXSIZE = 128
20 |
21 | # Lock to prevent multiple threads from accessing the cache at same time.
22 | cache_access_lock = threading.RLock()
23 |
24 | cache_type = os.environ.get("PREMAILER_CACHE", "LFU")
25 | if cache_type not in CACHE_IMPLEMENTATIONS:
26 | raise ValueError(
27 | "Unsupported cache implementation. Available options: %s"
28 | % "/".join(CACHE_IMPLEMENTATIONS.keys())
29 | )
30 |
31 | cache_init_options = {
32 | "maxsize": int(os.environ.get("PREMAILER_CACHE_MAXSIZE", DEFAULT_CACHE_MAXSIZE))
33 | }
34 | if cache_type == "TTL":
35 | cache_init_options["ttl"] = int(
36 | os.environ.get("PREMAILER_CACHE_TTL", TTL_CACHE_TIMEOUT)
37 | )
38 |
39 | cache = CACHE_IMPLEMENTATIONS[cache_type](**cache_init_options)
40 |
41 |
42 | def function_cache(**options):
43 | def decorator(func):
44 | @cachetools.cached(cache, lock=cache_access_lock)
45 | @functools.wraps(func)
46 | def inner(*args, **kwargs):
47 | return func(*args, **kwargs)
48 |
49 | return inner
50 |
51 | return decorator
52 |
--------------------------------------------------------------------------------
/premailer/merge_style.py:
--------------------------------------------------------------------------------
1 | import cssutils
2 | import threading
3 | from collections import OrderedDict
4 |
5 | from premailer.cache import function_cache
6 |
7 |
8 | def format_value(prop):
9 | if prop.priority == "important":
10 | return prop.propertyValue.cssText.strip() + " !important"
11 | else:
12 | return prop.propertyValue.cssText.strip()
13 |
14 |
15 | @function_cache()
16 | def csstext_to_pairs(csstext, validate=True):
17 | """
18 | csstext_to_pairs takes css text and make it to list of
19 | tuple of key,value.
20 | """
21 | # The lock is required to avoid ``cssutils`` concurrency
22 | # issues documented in issue #65
23 | with csstext_to_pairs._lock:
24 | return [
25 | (prop.name.strip(), format_value(prop))
26 | for prop in cssutils.parseStyle(csstext, validate=validate)
27 | ]
28 |
29 |
30 | csstext_to_pairs._lock = threading.RLock()
31 |
32 |
33 | def merge_styles(inline_style, new_styles, classes, remove_unset_properties=False):
34 | """
35 | This will merge all new styles where the order is important
36 | The last one will override the first
37 | When that is done it will apply old inline style again
38 | The old inline style is always important and override
39 | all new ones. The inline style must be valid.
40 |
41 | Args:
42 | inline_style(str): the old inline style of the element if there
43 | is one
44 | new_styles: a list of new styles, each element should be
45 | a list of tuple
46 | classes: a list of classes which maps new_styles, important!
47 | remove_unset_properties(bool): Allow us to remove certain CSS
48 | properties with rules that set their value to 'unset'
49 |
50 | Returns:
51 | str: the final style
52 | """
53 | # building classes
54 | styles = OrderedDict([("", OrderedDict())])
55 | for pc in set(classes):
56 | styles[pc] = OrderedDict()
57 |
58 | for i, style in enumerate(new_styles):
59 | for k, v in style:
60 | styles[classes[i]][k] = v
61 |
62 | # keep always the old inline style
63 | if inline_style:
64 | # inline should be a declaration list as I understand
65 | # ie property-name:property-value;...
66 | for k, v in csstext_to_pairs(inline_style):
67 | styles[""][k] = v
68 |
69 | normal_styles = []
70 | pseudo_styles = []
71 | for pseudoclass, kv in styles.items():
72 | if remove_unset_properties:
73 | # Remove rules that we were going to have value 'unset' because
74 | # they effectively are the same as not saying anything about the
75 | # property when inlined
76 | kv = OrderedDict(
77 | (k, v) for (k, v) in kv.items() if not v.lower() == "unset"
78 | )
79 | if not kv:
80 | continue
81 | if pseudoclass:
82 | pseudo_styles.append(
83 | "%s{%s}"
84 | % (pseudoclass, "; ".join("%s:%s" % (k, v) for k, v in kv.items()))
85 | )
86 | else:
87 | normal_styles.append("; ".join("%s:%s" % (k, v) for k, v in kv.items()))
88 |
89 | if pseudo_styles:
90 | # if we do or code thing correct this should not happen
91 | # inline style definition: declarations without braces
92 | all_styles = (
93 | (["{%s}" % "".join(normal_styles)] + pseudo_styles)
94 | if normal_styles
95 | else pseudo_styles
96 | )
97 | else:
98 | all_styles = normal_styles
99 |
100 | return " ".join(all_styles).strip()
101 |
--------------------------------------------------------------------------------
/premailer/premailer.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import operator
3 | import os
4 | import re
5 | import warnings
6 | from collections import OrderedDict
7 | from html import escape, unescape
8 | from urllib.parse import urljoin, urlparse, unquote
9 |
10 | import cssutils
11 | import requests
12 | from lxml import etree
13 | from lxml.cssselect import CSSSelector
14 |
15 | from premailer.cache import function_cache
16 | from premailer.merge_style import csstext_to_pairs, merge_styles
17 |
18 |
19 | __all__ = ["PremailerError", "Premailer", "transform"]
20 |
21 |
22 | class PremailerError(Exception):
23 | pass
24 |
25 |
26 | class ExternalNotFoundError(ValueError):
27 | pass
28 |
29 |
30 | class ExternalFileLoadingError(Exception):
31 | pass
32 |
33 |
34 | def make_important(bulk):
35 | """makes every property in a string !important."""
36 | return ";".join(
37 | "%s !important" % p if not p.endswith("!important") else p
38 | for p in bulk.split(";")
39 | )
40 |
41 |
42 | def get_or_create_head(root):
43 | """Ensures that `root` contains a element and returns it."""
44 | head = _create_cssselector("head")(root)
45 | if not head:
46 | head = etree.Element("head")
47 | body = _create_cssselector("body")(root)[0]
48 | body.getparent().insert(0, head)
49 | return head
50 | else:
51 | return head[0]
52 |
53 |
54 | @function_cache()
55 | def _cache_parse_css_string(css_body, validate=True):
56 | """
57 | This function will cache the result from cssutils
58 | It is a big gain when number of rules is big
59 | Maximum cache entries are 1000. This is mainly for
60 | protecting memory leak in case something gone wild.
61 | Be aware that you can turn the cache off in Premailer
62 |
63 | Args:
64 | css_body(str): css rules in string format
65 | validate(bool): if cssutils should validate
66 |
67 | Returns:
68 | cssutils.css.cssstylesheet.CSSStyleSheet
69 |
70 | """
71 | return cssutils.parseString(css_body, validate=validate)
72 |
73 |
74 | @function_cache()
75 | def _create_cssselector(selector):
76 | return CSSSelector(selector)
77 |
78 |
79 | def capitalize_float_margin(css_body):
80 | """Capitalize float and margin CSS property names"""
81 |
82 | def _capitalize_property(match):
83 | return "{0}:{1}{2}".format(
84 | match.group("property").capitalize(),
85 | match.group("value"),
86 | match.group("terminator"),
87 | )
88 |
89 | return _lowercase_margin_float_rule.sub(_capitalize_property, css_body)
90 |
91 |
92 | _element_selector_regex = re.compile(r"(^|\s)\w")
93 | _cdata_regex = re.compile(r"\<\!\[CDATA\[(.*?)\]\]\>", re.DOTALL)
94 | _lowercase_margin_float_rule = re.compile(
95 | r"""(?Pmargin(-(top|bottom|left|right))?|float)
96 | :
97 | (?P.*?)
98 | (?P$|;)""",
99 | re.IGNORECASE | re.VERBOSE,
100 | )
101 | _importants = re.compile(r"\s*!important")
102 | #: The short (3-digit) color codes that cause issues for IBM Notes
103 | _short_color_codes = re.compile(r"^#([0-9a-f])([0-9a-f])([0-9a-f])$", re.I)
104 |
105 | # These selectors don't apply to all elements. Rather, they specify
106 | # which elements to apply to.
107 | FILTER_PSEUDOSELECTORS = [":last-child", ":first-child", ":nth-child"]
108 |
109 |
110 | class Premailer(object):
111 |
112 | attribute_name = "data-premailer"
113 |
114 | def __init__(
115 | self,
116 | html=None,
117 | base_url=None,
118 | disable_link_rewrites=False,
119 | preserve_internal_links=False,
120 | preserve_inline_attachments=True,
121 | preserve_handlebar_syntax=False,
122 | exclude_pseudoclasses=True,
123 | keep_style_tags=False,
124 | include_star_selectors=False,
125 | remove_classes=False,
126 | capitalize_float_margin=False,
127 | strip_important=True,
128 | external_styles=None,
129 | css_text=None,
130 | method="html",
131 | base_path=None,
132 | disable_basic_attributes=None,
133 | disable_validation=False,
134 | cache_css_parsing=True,
135 | cssutils_logging_handler=None,
136 | cssutils_logging_level=None,
137 | disable_leftover_css=False,
138 | align_floating_images=True,
139 | remove_unset_properties=True,
140 | allow_network=True,
141 | allow_insecure_ssl=False,
142 | allow_loading_external_files=False,
143 | session=None,
144 | ):
145 | self.html = html
146 | self.base_url = base_url
147 |
148 | # If base_url is specified, it is used for loading external stylesheets
149 | # via relative URLs.
150 | #
151 | # Also, if base_url is specified, premailer will transform all URLs by
152 | # joining them with the base_url. Setting preserve_internal_links to
153 | # True will disable this behavior for links to named anchors. Setting
154 | # preserve_inline_attachments to True will disable this behavior for
155 | # any links with cid: scheme. Setting disable_link_rewrites to True
156 | # will disable this behavior altogether.
157 | self.disable_link_rewrites = disable_link_rewrites
158 | self.preserve_internal_links = preserve_internal_links
159 | self.preserve_inline_attachments = preserve_inline_attachments
160 | self.preserve_handlebar_syntax = preserve_handlebar_syntax
161 | self.exclude_pseudoclasses = exclude_pseudoclasses
162 | # whether to delete the
724 |
725 |
726 |