2014-09-17 03:35:10 +02:00
|
|
|
from __future__ import absolute_import, unicode_literals
|
2013-11-18 06:25:18 +01:00
|
|
|
import sys
|
2009-07-15 13:00:27 +02:00
|
|
|
import re
|
2014-06-29 05:53:04 +02:00
|
|
|
import unittest
|
2015-06-10 18:40:35 +02:00
|
|
|
import logging
|
2013-11-18 06:25:18 +01:00
|
|
|
from contextlib import contextmanager
|
2014-09-24 04:54:16 +02:00
|
|
|
if sys.version_info >= (3, ): # As in, Python 3
|
|
|
|
from urllib.request import urlopen
|
|
|
|
else: # Python 2
|
|
|
|
from urllib2 import urlopen
|
2015-02-19 20:24:26 +01:00
|
|
|
urlopen = urlopen # shut up pyflakes
|
2016-03-15 19:13:53 +01:00
|
|
|
from io import StringIO # Yes, the is an io lib in py2.x
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
from nose.tools import eq_, ok_, assert_raises
|
2014-04-02 05:57:56 +02:00
|
|
|
import mock
|
2014-12-11 18:36:33 +01:00
|
|
|
from lxml.etree import fromstring, XMLSyntaxError
|
2014-08-22 00:13:03 +02:00
|
|
|
|
2014-09-28 20:11:22 +02:00
|
|
|
from premailer.premailer import (
|
2014-08-22 00:13:03 +02:00
|
|
|
transform,
|
|
|
|
Premailer,
|
|
|
|
merge_styles,
|
2015-02-15 19:16:45 +01:00
|
|
|
csstext_to_pairs,
|
2014-09-24 04:54:16 +02:00
|
|
|
ExternalNotFoundError,
|
2014-08-22 00:13:03 +02:00
|
|
|
)
|
2014-09-28 20:11:22 +02:00
|
|
|
from premailer.__main__ import main
|
2014-10-08 19:19:37 +02:00
|
|
|
import premailer.premailer # lint:ok
|
2013-11-18 06:25:18 +01:00
|
|
|
|
|
|
|
|
2013-11-18 17:35:47 +01:00
|
|
|
whitespace_between_tags = re.compile('>\s*<')
|
|
|
|
|
|
|
|
|
2013-11-18 06:25:18 +01:00
|
|
|
@contextmanager
|
|
|
|
def captured_output():
|
|
|
|
new_out, new_err = StringIO(), StringIO()
|
|
|
|
old_out, old_err = sys.stdout, sys.stderr
|
|
|
|
try:
|
|
|
|
sys.stdout, sys.stderr = new_out, new_err
|
|
|
|
yield sys.stdout, sys.stderr
|
|
|
|
finally:
|
|
|
|
sys.stdout, sys.stderr = old_out, old_err
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def provide_input(content):
|
|
|
|
old_stdin = sys.stdin
|
|
|
|
sys.stdin = StringIO(content)
|
|
|
|
try:
|
|
|
|
with captured_output() as (out, err):
|
|
|
|
yield out, err
|
|
|
|
finally:
|
|
|
|
sys.stdin = old_stdin
|
|
|
|
sys.stdin = StringIO(content)
|
2011-10-15 09:50:14 +02:00
|
|
|
|
2009-07-15 13:00:27 +02:00
|
|
|
|
2016-03-15 19:13:53 +01:00
|
|
|
class MockResponse(object):
|
2012-12-13 16:57:52 +01:00
|
|
|
|
2016-03-15 19:13:53 +01:00
|
|
|
def __init__(self, content):
|
|
|
|
self.text = content
|
2012-01-03 21:01:32 +01:00
|
|
|
|
2014-09-24 04:54:16 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def compare_html(one, two):
|
|
|
|
one = one.strip()
|
|
|
|
two = two.strip()
|
|
|
|
one = whitespace_between_tags.sub('>\n<', one)
|
|
|
|
two = whitespace_between_tags.sub('>\n<', two)
|
|
|
|
one = one.replace('><', '>\n<')
|
|
|
|
two = two.replace('><', '>\n<')
|
|
|
|
for i, line in enumerate(one.splitlines()):
|
|
|
|
other = two.splitlines()[i]
|
|
|
|
if line.lstrip() != other.lstrip():
|
|
|
|
eq_(line.lstrip(), other.lstrip())
|
|
|
|
|
2014-09-24 04:54:16 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
class Tests(unittest.TestCase):
|
|
|
|
|
|
|
|
def shortDescription(self):
|
|
|
|
# most annoying thing in the world about nose
|
|
|
|
pass
|
|
|
|
|
|
|
|
def test_merge_styles_basic(self):
|
2015-02-15 19:16:45 +01:00
|
|
|
inline_style = 'font-size:1px; color: red'
|
2014-06-29 05:53:04 +02:00
|
|
|
new = 'font-size:2px; font-weight: bold'
|
2016-01-22 16:26:12 +01:00
|
|
|
expect = 'font-size:1px;', 'font-weight:bold;', 'color:red'
|
2015-02-19 20:24:26 +01:00
|
|
|
result = merge_styles(inline_style, [csstext_to_pairs(new)], [''])
|
2014-06-29 05:53:04 +02:00
|
|
|
for each in expect:
|
|
|
|
ok_(each in result)
|
|
|
|
|
|
|
|
def test_merge_styles_with_class(self):
|
2015-02-15 19:16:45 +01:00
|
|
|
inline_style = 'color:red; font-size:1px;'
|
2014-06-29 05:53:04 +02:00
|
|
|
new, class_ = 'font-size:2px; font-weight: bold', ':hover'
|
|
|
|
|
|
|
|
# because we're dealing with dicts (random order) we have to
|
|
|
|
# test carefully.
|
|
|
|
# We expect something like this:
|
|
|
|
# {color:red; font-size:1px} :hover{font-size:2px; font-weight:bold}
|
|
|
|
|
2015-02-15 19:16:45 +01:00
|
|
|
result = merge_styles(inline_style, [csstext_to_pairs(new)], [class_])
|
2014-06-29 05:53:04 +02:00
|
|
|
ok_(result.startswith('{'))
|
|
|
|
ok_(result.endswith('}'))
|
|
|
|
ok_(' :hover{' in result)
|
|
|
|
split_regex = re.compile('{([^}]+)}')
|
|
|
|
eq_(len(split_regex.findall(result)), 2)
|
|
|
|
expect_first = 'color:red', 'font-size:1px'
|
|
|
|
expect_second = 'font-weight:bold', 'font-size:2px'
|
|
|
|
for each in expect_first:
|
|
|
|
ok_(each in split_regex.findall(result)[0])
|
|
|
|
for each in expect_second:
|
|
|
|
ok_(each in split_regex.findall(result)[1])
|
|
|
|
|
|
|
|
def test_merge_styles_non_trivial(self):
|
2015-02-19 20:24:26 +01:00
|
|
|
inline_style = (
|
|
|
|
'background-image:url("data:image/png;base64,iVBORw0KGg")'
|
|
|
|
)
|
2014-06-29 05:53:04 +02:00
|
|
|
new = 'font-size:2px; font-weight: bold'
|
|
|
|
expect = (
|
|
|
|
'background-image:url("data:image/png;base64,iVBORw0KGg")',
|
|
|
|
'font-size:2px;',
|
|
|
|
'font-weight:bold'
|
|
|
|
)
|
2015-02-19 20:24:26 +01:00
|
|
|
result = merge_styles(inline_style, [csstext_to_pairs(new)], [''])
|
2014-06-29 05:53:04 +02:00
|
|
|
for each in expect:
|
2014-09-24 04:54:16 +02:00
|
|
|
ok_(each in result)
|
2014-06-29 05:53:04 +02:00
|
|
|
|
2015-09-09 22:37:38 +02:00
|
|
|
def test_merge_styles_with_unset(self):
|
|
|
|
inline_style = 'color: red'
|
|
|
|
new = 'font-size: 10px; font-size: unset; font-weight: bold'
|
2016-01-22 16:26:12 +01:00
|
|
|
expect = 'font-weight:bold;', 'color:red'
|
2015-09-09 22:37:38 +02:00
|
|
|
css_new = csstext_to_pairs(new)
|
|
|
|
result = merge_styles(
|
|
|
|
inline_style,
|
|
|
|
[css_new],
|
|
|
|
[''],
|
|
|
|
remove_unset_properties=True,
|
|
|
|
)
|
|
|
|
for each in expect:
|
|
|
|
ok_(each in result)
|
|
|
|
ok_('font-size' not in result)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_basic_html(self):
|
|
|
|
"""test the simplest case"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2012-01-03 21:01:32 +01:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:red">Hi!</h1>
|
|
|
|
<p><strong style="text-decoration:none">Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2012-01-03 21:01:32 +01:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
compare_html(expect_html, result_html)
|
2014-08-22 00:13:03 +02:00
|
|
|
|
2016-06-07 15:59:45 +02:00
|
|
|
def test_remove_classes(self):
|
|
|
|
"""test the simplest case"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
.stuff {
|
|
|
|
color: red;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p class="stuff"><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p style="color:red"><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, remove_classes=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
def test_basic_html_shortcut_function(self):
|
|
|
|
"""test the plain transform function"""
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:red">Hi!</h1>
|
|
|
|
<p><strong style="text-decoration:none">Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
result_html = transform(html)
|
|
|
|
compare_html(expect_html, result_html)
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_empty_style_tag(self):
|
|
|
|
"""empty style tag"""
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title></title>
|
|
|
|
<style type="text/css"></style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title></title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
2012-05-28 05:01:46 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
compare_html(expect_html, result_html)
|
2012-07-25 09:08:24 +02:00
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
def test_include_star_selector(self):
|
|
|
|
"""test the simplest case"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
p * { color: red }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html_not_included = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html_not_included, result_html)
|
|
|
|
|
|
|
|
expect_html_star_included = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong style="color:red">Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, include_star_selectors=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html_star_included, result_html)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_mixed_pseudo_selectors(self):
|
|
|
|
"""mixing pseudo selectors with straight forward selectors"""
|
2013-07-08 23:10:24 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
html = """<html>
|
2014-04-02 06:13:21 +02:00
|
|
|
<head>
|
2014-06-29 05:53:04 +02:00
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
p { color: yellow }
|
|
|
|
a { color: blue }
|
|
|
|
a:hover { color: pink }
|
|
|
|
</style>
|
2014-04-02 06:13:21 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2014-06-29 05:53:04 +02:00
|
|
|
<p>
|
|
|
|
<a href="#">Page</a>
|
|
|
|
</p>
|
2014-04-02 06:13:21 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
expect_html = """<html>
|
2014-04-02 06:13:21 +02:00
|
|
|
<head>
|
2014-06-29 05:53:04 +02:00
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">a:hover {color:pink}</style>
|
2014-04-02 06:13:21 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2014-06-29 05:53:04 +02:00
|
|
|
<p style="color:yellow"><a href="#" style="color:blue">Page</a></p>
|
2014-04-02 06:13:21 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_basic_html_with_pseudo_selector(self):
|
|
|
|
"""test the simplest case"""
|
|
|
|
|
|
|
|
html = """
|
|
|
|
<html>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { border:1px solid black }
|
|
|
|
p { color:red;}
|
|
|
|
p::first-letter { float:left; }
|
|
|
|
</style>
|
|
|
|
<h1 style="font-weight:bolder">Peter</h1>
|
|
|
|
<p>Hej</p>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">p::first-letter {float:left}</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="border:1px solid black; font-weight:bolder">Peter</h1>
|
|
|
|
<p style="color:red">Hej</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_parse_style_rules(self):
|
|
|
|
p = Premailer('html') # won't need the html
|
|
|
|
func = p._parse_style_rules
|
|
|
|
rules, leftover = func("""
|
2013-06-06 20:18:50 +02:00
|
|
|
h1, h2 { color:red; }
|
2014-06-29 05:53:04 +02:00
|
|
|
/* ignore
|
|
|
|
this */
|
2013-06-06 20:18:50 +02:00
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
2014-06-29 05:53:04 +02:00
|
|
|
ul li { list-style: 2px; }
|
|
|
|
a:hover { text-decoration: underline }
|
|
|
|
""", 0)
|
|
|
|
|
|
|
|
# 'rules' is a list, turn it into a dict for
|
|
|
|
# easier assertion testing
|
|
|
|
rules_dict = {}
|
|
|
|
rules_specificity = {}
|
|
|
|
for specificity, k, v in rules:
|
|
|
|
rules_dict[k] = v
|
|
|
|
rules_specificity[k] = specificity
|
|
|
|
|
|
|
|
ok_('h1' in rules_dict)
|
|
|
|
ok_('h2' in rules_dict)
|
|
|
|
ok_('strong' in rules_dict)
|
|
|
|
ok_('ul li' in rules_dict)
|
|
|
|
|
|
|
|
eq_(rules_dict['h1'], 'color:red')
|
|
|
|
eq_(rules_dict['h2'], 'color:red')
|
|
|
|
eq_(rules_dict['strong'], 'text-decoration:none')
|
|
|
|
eq_(rules_dict['ul li'], 'list-style:2px')
|
|
|
|
ok_('a:hover' not in rules_dict)
|
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
# won't need the html
|
|
|
|
p = Premailer('html', exclude_pseudoclasses=True)
|
2014-06-29 05:53:04 +02:00
|
|
|
func = p._parse_style_rules
|
|
|
|
rules, leftover = func("""
|
|
|
|
ul li { list-style: 2px; }
|
|
|
|
a:hover { text-decoration: underline }
|
|
|
|
""", 0)
|
|
|
|
|
|
|
|
eq_(len(rules), 1)
|
|
|
|
specificity, k, v = rules[0]
|
|
|
|
eq_(k, 'ul li')
|
|
|
|
eq_(v, 'list-style:2px')
|
|
|
|
|
|
|
|
eq_(len(leftover), 1)
|
|
|
|
k, v = leftover[0]
|
|
|
|
eq_((k, v), ('a:hover', 'text-decoration:underline'), (k, v))
|
|
|
|
|
|
|
|
def test_precedence_comparison(self):
|
|
|
|
p = Premailer('html') # won't need the html
|
|
|
|
rules, leftover = p._parse_style_rules("""
|
|
|
|
#identified { color:blue; }
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
ul li { list-style: 2px; }
|
|
|
|
li.example { color:green; }
|
|
|
|
strong { text-decoration:none }
|
|
|
|
div li.example p.sample { color:black; }
|
|
|
|
""", 0)
|
|
|
|
|
|
|
|
# 'rules' is a list, turn it into a dict for
|
|
|
|
# easier assertion testing
|
|
|
|
rules_specificity = {}
|
|
|
|
for specificity, k, v in rules:
|
|
|
|
rules_specificity[k] = specificity
|
|
|
|
|
|
|
|
# Last in file wins
|
|
|
|
ok_(rules_specificity['h1'] < rules_specificity['h2'])
|
|
|
|
# More elements wins
|
|
|
|
ok_(rules_specificity['strong'] < rules_specificity['ul li'])
|
|
|
|
# IDs trump everything
|
|
|
|
ok_(rules_specificity['div li.example p.sample'] <
|
|
|
|
rules_specificity['#identified'])
|
2014-05-23 20:37:33 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
# Classes trump multiple elements
|
|
|
|
ok_(rules_specificity['ul li'] <
|
|
|
|
rules_specificity['li.example'])
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_base_url_fixer(self):
|
|
|
|
"""if you leave some URLS as /foo and set base_url to
|
|
|
|
'http://www.google.com' the URLS become 'http://www.google.com/foo'
|
|
|
|
"""
|
|
|
|
html = '''<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="/images/foo.jpg">
|
|
|
|
<img src="/images/bar.gif">
|
2014-08-22 00:13:03 +02:00
|
|
|
<img src="cid:images/baz.gif">
|
2014-06-29 05:53:04 +02:00
|
|
|
<img src="http://www.googe.com/photos/foo.jpg">
|
|
|
|
<a href="/home">Home</a>
|
|
|
|
<a href="http://www.peterbe.com">External</a>
|
|
|
|
<a href="subpage">Subpage</a>
|
|
|
|
<a href="#internal_link">Internal Link</a>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
'''
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
expect_html = '''<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="http://kungfupeople.com/images/foo.jpg">
|
|
|
|
<img src="http://kungfupeople.com/images/bar.gif">
|
2014-08-22 00:13:03 +02:00
|
|
|
<img src="cid:images/baz.gif">
|
2014-06-29 05:53:04 +02:00
|
|
|
<img src="http://www.googe.com/photos/foo.jpg">
|
|
|
|
<a href="http://kungfupeople.com/home">Home</a>
|
|
|
|
<a href="http://www.peterbe.com">External</a>
|
|
|
|
<a href="http://kungfupeople.com/subpage">Subpage</a>
|
|
|
|
<a href="#internal_link">Internal Link</a>
|
|
|
|
</body>
|
|
|
|
</html>'''
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
base_url='http://kungfupeople.com',
|
|
|
|
preserve_internal_links=True
|
|
|
|
)
|
2014-06-29 05:53:04 +02:00
|
|
|
result_html = p.transform()
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
compare_html(expect_html, result_html)
|
2014-04-02 05:57:56 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_base_url_with_path(self):
|
|
|
|
"""if you leave some URLS as /foo and set base_url to
|
|
|
|
'http://www.google.com' the URLS become 'http://www.google.com/foo'
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = '''<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="/images/foo.jpg">
|
|
|
|
<img src="http://www.googe.com/photos/foo.jpg">
|
|
|
|
<a href="/home">Home</a>
|
|
|
|
<a href="http://www.peterbe.com">External</a>
|
|
|
|
<a href="http://www.peterbe.com/base/">External 2</a>
|
|
|
|
<a href="subpage">Subpage</a>
|
|
|
|
<a href="#internal_link">Internal Link</a>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
'''
|
|
|
|
|
|
|
|
expect_html = '''<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
2015-05-08 18:37:52 +02:00
|
|
|
<img src="http://kungfupeople.com/images/foo.jpg">
|
2014-06-29 05:53:04 +02:00
|
|
|
<img src="http://www.googe.com/photos/foo.jpg">
|
2015-05-08 18:37:52 +02:00
|
|
|
<a href="http://kungfupeople.com/home">Home</a>
|
2014-06-29 05:53:04 +02:00
|
|
|
<a href="http://www.peterbe.com">External</a>
|
|
|
|
<a href="http://www.peterbe.com/base/">External 2</a>
|
|
|
|
<a href="http://kungfupeople.com/base/subpage">Subpage</a>
|
|
|
|
<a href="#internal_link">Internal Link</a>
|
|
|
|
</body>
|
|
|
|
</html>'''
|
|
|
|
|
2015-05-08 18:37:52 +02:00
|
|
|
p = Premailer(html, base_url='http://kungfupeople.com/base/',
|
2014-06-29 05:53:04 +02:00
|
|
|
preserve_internal_links=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_style_block_with_external_urls(self):
|
|
|
|
"""
|
|
|
|
From http://github.com/peterbe/premailer/issues/#issue/2
|
|
|
|
|
|
|
|
If you have
|
|
|
|
body { background:url(http://example.com/bg.png); }
|
|
|
|
the ':' inside '://' is causing a problem
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
body {
|
|
|
|
color:#123;
|
|
|
|
background: url(http://example.com/bg.png);
|
|
|
|
font-family: Omerta;
|
2014-05-23 20:37:33 +02:00
|
|
|
}
|
|
|
|
</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
expect_html = """<html>
|
2014-06-29 05:53:04 +02:00
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
2015-02-19 20:24:26 +01:00
|
|
|
<body style="background:url(http://exam
|
|
|
|
ple.com/bg.png); color:#123; font-family:Omerta">
|
2014-06-29 05:53:04 +02:00
|
|
|
<h1>Hi!</h1>
|
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('exam\nple', 'example')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_shortcut_function(self):
|
|
|
|
# you don't have to use this approach:
|
|
|
|
# from premailer import Premailer
|
|
|
|
# p = Premailer(html, base_url=base_url)
|
|
|
|
# print p.transform()
|
|
|
|
# You can do it this way:
|
|
|
|
# from premailer import transform
|
|
|
|
# print transform(html, base_url=base_url)
|
|
|
|
|
|
|
|
html = '''<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">h1{color:#123}</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
</body>
|
|
|
|
</html>'''
|
|
|
|
|
|
|
|
expect_html = '''<html>
|
|
|
|
<head></head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:#123">Hi!</h1>
|
|
|
|
</body>
|
|
|
|
</html>'''
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-09-29 20:21:12 +02:00
|
|
|
def fragment_in_html(self, fragment, html, fullMessage=False):
|
|
|
|
if fullMessage:
|
|
|
|
message = '"{0}" not in\n{1}'.format(fragment, html)
|
|
|
|
else:
|
|
|
|
message = '"{0}" not in HTML'.format(fragment)
|
|
|
|
ok_(fragment in html, message)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_css_with_pseudoclasses_included(self):
|
|
|
|
"Pick up the pseudoclasses too and include them"
|
|
|
|
|
|
|
|
html = '''<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
a.special:link { text-decoration:none; }
|
|
|
|
a { color:red; }
|
|
|
|
a:hover { text-decoration:none; }
|
|
|
|
a,a:hover,
|
|
|
|
a:visited { border:1px solid green; }
|
|
|
|
p::first-letter {float: left; font-size: 300%}
|
2014-06-24 05:29:01 +02:00
|
|
|
</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="#" class="special">Special!</a>
|
|
|
|
<a href="#">Page</a>
|
|
|
|
<p>Paragraph</p>
|
|
|
|
</body>
|
|
|
|
</html>'''
|
2014-06-26 19:14:49 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
p = Premailer(html, exclude_pseudoclasses=False)
|
|
|
|
result_html = p.transform()
|
|
|
|
# because we're dealing with random dicts here we can't predict what
|
2014-09-29 20:21:12 +02:00
|
|
|
# order the style attribute will be written in so we'll look for
|
|
|
|
# things manually.
|
2014-10-01 04:13:01 +02:00
|
|
|
e = '<p style="::first-letter{float:left; font-size:300%}">'\
|
2014-09-29 20:21:12 +02:00
|
|
|
'Paragraph</p>'
|
|
|
|
self.fragment_in_html(e, result_html, True)
|
|
|
|
|
2016-01-22 16:26:12 +01:00
|
|
|
e = 'style="{color:red; border:1px solid green}'
|
2014-10-01 04:13:01 +02:00
|
|
|
self.fragment_in_html(e, result_html)
|
2014-09-29 20:21:12 +02:00
|
|
|
e = ' :visited{border:1px solid green}'
|
2014-10-01 04:13:01 +02:00
|
|
|
self.fragment_in_html(e, result_html)
|
2016-01-22 16:26:12 +01:00
|
|
|
e = ' :hover{text-decoration:none; border:1px solid green}'
|
2014-10-01 04:13:01 +02:00
|
|
|
self.fragment_in_html(e, result_html)
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
def test_css_with_pseudoclasses_excluded(self):
|
|
|
|
"Skip things like `a:hover{}` and keep them in the style block"
|
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
html = """<html>
|
2014-06-29 05:53:04 +02:00
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
a { color:red; }
|
|
|
|
a:hover { text-decoration:none; }
|
|
|
|
a,a:hover,
|
|
|
|
a:visited { border:1px solid green; }
|
|
|
|
p::first-letter {float: left; font-size: 300%}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="#">Page</a>
|
|
|
|
<p>Paragraph</p>
|
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>"""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">a:hover {text-decoration:none}
|
|
|
|
a:hover {border:1px solid green}
|
|
|
|
a:visited {border:1px solid green}p::first-letter {float:left;font-size:300%}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-01-22 16:26:12 +01:00
|
|
|
<a href="#" style="color:red; border:1px solid green">Page</a>
|
2015-02-19 20:24:26 +01:00
|
|
|
<p>Paragraph</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html, exclude_pseudoclasses=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
expect_html = whitespace_between_tags.sub('><', expect_html).strip()
|
|
|
|
result_html = whitespace_between_tags.sub('><', result_html).strip()
|
|
|
|
|
|
|
|
expect_html = re.sub('}\s+', '}', expect_html)
|
|
|
|
result_html = result_html.replace('}\n', '}')
|
|
|
|
|
|
|
|
eq_(expect_html, result_html)
|
|
|
|
# XXX
|
|
|
|
|
|
|
|
def test_css_with_html_attributes(self):
|
|
|
|
"""Some CSS styles can be applied as normal HTML attribute like
|
|
|
|
'background-color' can be turned into 'bgcolor'
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
2014-09-25 21:37:32 +02:00
|
|
|
td { background-color:red; vertical-align:middle;}
|
|
|
|
p { text-align:center;}
|
2014-06-29 05:53:04 +02:00
|
|
|
table { width:200px; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p>Text</p>
|
|
|
|
<table>
|
|
|
|
<tr>
|
|
|
|
<td>Cell 1</td>
|
|
|
|
<td>Cell 2</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p style="text-align:center" align="center">Text</p>
|
|
|
|
<table style="width:200px" width="200">
|
|
|
|
<tr>
|
2015-02-19 20:24:26 +01:00
|
|
|
<td style="background-color:red; vert
|
|
|
|
ical-align:middle" bgcolor="red" valign="middle">Cell 1</td>
|
|
|
|
<td style="background-color:red; vert
|
|
|
|
ical-align:middle" bgcolor="red" valign="middle">Cell 2</td>
|
2014-06-29 05:53:04 +02:00
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('vert\nical', 'vertical')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html, exclude_pseudoclasses=True)
|
|
|
|
result_html = p.transform()
|
2014-06-26 19:14:49 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
expect_html = re.sub('}\s+', '}', expect_html)
|
|
|
|
result_html = result_html.replace('}\n', '}')
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_css_disable_basic_html_attributes(self):
|
|
|
|
"""Some CSS styles can be applied as normal HTML attribute like
|
|
|
|
'background-color' can be turned into 'bgcolor'
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
td { background-color:red; }
|
|
|
|
p { text-align:center; }
|
|
|
|
table { width:200px; height: 300px; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p>Text</p>
|
|
|
|
<table>
|
|
|
|
<tr>
|
|
|
|
<td>Cell 1</td>
|
|
|
|
<td>Cell 2</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p style="text-align:center">Text</p>
|
|
|
|
<table style="height:300px; width:200px">
|
|
|
|
<tr>
|
|
|
|
<td style="background-color:red" bgcolor="red">Cell 1</td>
|
|
|
|
<td style="background-color:red" bgcolor="red">Cell 2</td>
|
|
|
|
</tr>
|
|
|
|
</table>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
exclude_pseudoclasses=True,
|
|
|
|
disable_basic_attributes=['align', 'width', 'height']
|
|
|
|
)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
expect_html = re.sub('}\s+', '}', expect_html)
|
|
|
|
result_html = result_html.replace('}\n', '}')
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_apple_newsletter_example(self):
|
|
|
|
# stupidity test
|
|
|
|
import os
|
|
|
|
|
2014-09-24 06:07:56 +02:00
|
|
|
html_file = os.path.join('premailer', 'tests',
|
2014-06-29 05:53:04 +02:00
|
|
|
'test-apple-newsletter.html')
|
|
|
|
html = open(html_file).read()
|
|
|
|
|
|
|
|
p = Premailer(html, exclude_pseudoclasses=False,
|
|
|
|
keep_style_tags=True,
|
|
|
|
strip_important=False)
|
|
|
|
result_html = p.transform()
|
|
|
|
ok_('<html>' in result_html)
|
|
|
|
ok_('<style media="only screen and (max-device-width: 480px)" '
|
|
|
|
'type="text/css">\n'
|
2015-02-19 20:24:26 +01:00
|
|
|
'* {line-height: normal !important; '
|
|
|
|
'-webkit-text-size-adjust: 125%}\n'
|
2014-06-29 05:53:04 +02:00
|
|
|
'</style>' in result_html)
|
|
|
|
|
|
|
|
def test_mailto_url(self):
|
|
|
|
"""if you use URL with mailto: protocol, they should stay as mailto:
|
|
|
|
when baseurl is used
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="mailto:e-mail@example.com">e-mail@example.com</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="mailto:e-mail@example.com">e-mail@example.com</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, base_url='http://kungfupeople.com')
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2017-07-10 19:44:55 +02:00
|
|
|
def test_tel_url(self):
|
|
|
|
"""if you use URL with tel: protocol, it should stay as tel:
|
|
|
|
when baseurl is used
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="tel:202-555-0113">202-555-0113</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, base_url='http://kungfupeople.com')
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(result_html, html)
|
|
|
|
|
2015-10-20 11:40:49 +02:00
|
|
|
def test_uppercase_margin(self):
|
|
|
|
"""Option to comply with outlook.com
|
|
|
|
|
|
|
|
https://emailonacid.com/blog/article/email-development/outlook.com-does-support-margins
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<style>
|
|
|
|
h1 {margin: 0}
|
|
|
|
h2 {margin-top:0;margin-bottom:0;margin-left:0;margin-right:0}
|
|
|
|
</style>
|
|
|
|
<body>
|
|
|
|
<h1>a</h1>
|
|
|
|
<h2>
|
|
|
|
b
|
|
|
|
</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="Margin:0">a</h1>
|
|
|
|
<h2 style="Margin-bottom:0; Margin-left:0; Margin-right:0; Margin-top:0">
|
|
|
|
b
|
|
|
|
</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, capitalize_float_margin=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_strip_important(self):
|
|
|
|
"""Get rid of !important. Makes no sense inline."""
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
p {
|
|
|
|
height:100% !important;
|
|
|
|
width:100% !important;
|
2014-06-26 19:14:49 +02:00
|
|
|
}
|
2014-06-29 05:53:04 +02:00
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p>Paragraph</p>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
expect_html = """<html>
|
2015-02-19 20:24:26 +01:00
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p style="height:100%; width:100%" height="100%" width="100%">Paragraph</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html, strip_important=True)
|
|
|
|
result_html = p.transform()
|
2014-06-26 19:14:49 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_inline_wins_over_external(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
div {
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
/* This tests that a second loop for the same style still doesn't
|
|
|
|
* overwrite it. */
|
|
|
|
div {
|
|
|
|
text-align: left;
|
2014-06-26 19:14:49 +02:00
|
|
|
}
|
2014-06-29 05:53:04 +02:00
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right">Some text</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right" align="right">Some text</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
2014-06-26 19:14:49 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_last_child(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
div {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
div:last-child {
|
|
|
|
text-align: left;
|
2014-06-26 19:14:49 +02:00
|
|
|
}
|
|
|
|
</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div>First child</div>
|
|
|
|
<div>Last child</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right" align="right">First child</div>
|
|
|
|
<div style="text-align:left" align="left">Last child</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_last_child_exclude_pseudo(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
div {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
div:last-child {
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div>First child</div>
|
|
|
|
<div>Last child</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right" align="right">First child</div>
|
|
|
|
<div style="text-align:left" align="left">Last child</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, exclude_pseudoclasses=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_mediaquery(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
div {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
@media print{
|
|
|
|
div {
|
|
|
|
text-align: center;
|
|
|
|
color: white;
|
|
|
|
}
|
|
|
|
div {
|
|
|
|
font-size: 999px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div>First div</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">@media print {
|
|
|
|
div {
|
|
|
|
text-align: center !important;
|
|
|
|
color: white !important
|
|
|
|
}
|
|
|
|
div {
|
|
|
|
font-size: 999px !important
|
|
|
|
}
|
|
|
|
}</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right" align="right">First div</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, strip_important=False)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_child_selector(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">
|
|
|
|
body > div {
|
|
|
|
text-align: right;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div>First div</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div style="text-align:right" align="right">First div</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_doctype(self):
|
|
|
|
html = (
|
|
|
|
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
|
|
|
|
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
|
|
|
|
"""<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
)
|
|
|
|
|
|
|
|
expect_html = (
|
|
|
|
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
|
|
|
|
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
|
|
|
|
"""<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
)
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_prefer_inline_to_class(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
.example {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="example" style="color:red"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="example" style="color:red"></div>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_favour_rule_with_element_over_generic(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
div.example {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
.example {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="example"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="example" style="color:green"></div>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_favour_rule_with_class_over_generic(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
div.example {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
div {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="example"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="example" style="color:green"></div>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_favour_rule_with_id_over_others(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
#identified {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
div.example {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="example" id="identified"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="example" id="identified" style="color:green"></div>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-11-20 21:50:58 +01:00
|
|
|
def test_favour_rule_with_important_over_others(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
.makeblue {
|
|
|
|
color: blue !important;
|
|
|
|
font-size: 12px;
|
|
|
|
}
|
2016-06-07 15:59:45 +02:00
|
|
|
#id {
|
2014-11-20 21:50:58 +01:00
|
|
|
color: green;
|
|
|
|
font-size: 22px;
|
|
|
|
}
|
|
|
|
div.example {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="example makeblue" id="id"></div>
|
2014-11-20 21:50:58 +01:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
2016-06-07 15:59:45 +02:00
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="example makeblue" id="id" style="font-size:22px; color:blue"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-11-20 21:50:58 +01:00
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
2014-12-25 03:30:30 +01:00
|
|
|
compare_html(expect_html, result_html)
|
2014-11-20 21:50:58 +01:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_multiple_style_elements(self):
|
2015-02-19 20:24:26 +01:00
|
|
|
"""Asserts that rules from multiple style elements
|
|
|
|
are inlined correctly."""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:green; }
|
|
|
|
p {
|
|
|
|
font-size:120%
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:green">Hi!</h1>
|
2015-02-19 20:24:26 +01:00
|
|
|
<p style="font-size:120%"><strong style="text-deco
|
|
|
|
ration:none">Yes!</strong></p>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('deco\nration', 'decoration')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_style_attribute_specificity(self):
|
|
|
|
"""Stuff already in style attributes beats style tags."""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color: pink }
|
|
|
|
h1.foo { color: blue }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 class="foo" style="color: green">Hi!</h1>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<h1 class="foo" style="color:green">Hi!</h1>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_ignore_style_elements_with_media_attribute(self):
|
|
|
|
"""Asserts that style elements with media attributes other than
|
|
|
|
'screen' are ignored."""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<style type="text/css" media="screen">
|
|
|
|
h1, h2 { color:green; }
|
|
|
|
p {
|
|
|
|
font-size:16px;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<style type="text/css" media="only screen and (max-width: 480px)">
|
|
|
|
h1, h2 { color:orange; }
|
|
|
|
p {
|
|
|
|
font-size:120%;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css" media="only screen and (max-width: 480px)">
|
|
|
|
h1, h2 { color:orange; }
|
|
|
|
p {
|
|
|
|
font-size:120%;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:green">Hi!</h1>
|
2015-02-19 20:24:26 +01:00
|
|
|
<p style="font-size:16px"><strong style="text-deco
|
|
|
|
ration:none">Yes!</strong></p>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('deco\nration', 'decoration')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_leftover_important(self):
|
|
|
|
"""Asserts that leftover styles should be marked as !important."""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
a { color: red; }
|
|
|
|
a:hover { color: green; }
|
|
|
|
a:focus { color: blue !important; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="#">Hi!</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
2014-09-27 14:45:03 +02:00
|
|
|
<style type="text/css">
|
|
|
|
a { color: red; }
|
|
|
|
a:hover { color: green; }
|
|
|
|
a:focus { color: blue !important; }
|
|
|
|
</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<a href="#" style="color:red">Hi!</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html,
|
|
|
|
keep_style_tags=True,
|
|
|
|
strip_important=False)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_basic_xml(self):
|
|
|
|
"""Test the simplest case with xml"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
img { border: none; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="test.png" alt="test"/>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="test.png" alt="test" style="border:none"/>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
p = Premailer(html, method="xml")
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
def test_broken_xml(self):
|
|
|
|
"""Test the simplest case with xml"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title
|
|
|
|
<style type="text/css">
|
|
|
|
img { border: none; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="test.png" alt="test"/>
|
|
|
|
</body>
|
|
|
|
"""
|
|
|
|
|
|
|
|
p = Premailer(html, method="xml")
|
|
|
|
assert_raises(
|
|
|
|
XMLSyntaxError,
|
|
|
|
p.transform,
|
|
|
|
)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_xml_cdata(self):
|
|
|
|
"""Test that CDATA is set correctly on remaining styles"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
span:hover > a { background: red; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<span><a>Test</a></span>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
2015-02-19 20:24:26 +01:00
|
|
|
<style type="text/css">/*<![CDATA[*/span:hover > a {back
|
|
|
|
ground:red}/*]]>*/</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<span><a>Test</a></span>
|
|
|
|
</body>
|
|
|
|
</html>
|
2015-02-19 20:24:26 +01:00
|
|
|
""".replace('back\nground', 'background')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
p = Premailer(html, method="xml")
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_command_line_fileinput_from_stdin(self):
|
|
|
|
html = '<style>h1 { color:red; }</style><h1>Title</h1>'
|
|
|
|
expect_html = """
|
|
|
|
<html>
|
|
|
|
<head></head>
|
|
|
|
<body><h1 style="color:red">Title</h1></body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
with provide_input(html) as (out, err):
|
|
|
|
main([])
|
|
|
|
result_html = out.getvalue().strip()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_command_line_fileinput_from_argument(self):
|
|
|
|
with captured_output() as (out, err):
|
2014-08-22 00:13:03 +02:00
|
|
|
main([
|
|
|
|
'-f',
|
2014-09-28 20:11:22 +02:00
|
|
|
'premailer/tests/test-apple-newsletter.html',
|
2014-08-22 00:13:03 +02:00
|
|
|
'--disable-basic-attributes=bgcolor'
|
|
|
|
])
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
result_html = out.getvalue().strip()
|
|
|
|
|
|
|
|
ok_('<html>' in result_html)
|
|
|
|
ok_('<style media="only screen and (max-device-width: 480px)" '
|
|
|
|
'type="text/css">\n'
|
2015-02-19 20:24:26 +01:00
|
|
|
'* {line-height: normal !important; '
|
|
|
|
'-webkit-text-size-adjust: 125%}\n'
|
2014-06-29 05:53:04 +02:00
|
|
|
'</style>' in result_html)
|
|
|
|
|
2014-09-04 19:03:45 +02:00
|
|
|
def test_command_line_preserve_style_tags(self):
|
|
|
|
with captured_output() as (out, err):
|
|
|
|
main([
|
|
|
|
'-f',
|
2014-09-28 20:11:22 +02:00
|
|
|
'premailer/tests/test-issue78.html',
|
2014-09-29 23:37:02 +02:00
|
|
|
'--preserve-style-tags',
|
|
|
|
'--external-style=premailer/tests/test-external-styles.css',
|
2014-09-04 19:03:45 +02:00
|
|
|
])
|
|
|
|
|
|
|
|
result_html = out.getvalue().strip()
|
|
|
|
|
|
|
|
expect_html = """
|
|
|
|
<html>
|
|
|
|
<head>
|
2014-09-29 23:37:02 +02:00
|
|
|
<style type="text/css">h1 {
|
|
|
|
color: blue;
|
|
|
|
}
|
|
|
|
h2 {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
a {
|
|
|
|
color: pink;
|
|
|
|
}
|
|
|
|
a:hover {
|
|
|
|
color: purple;
|
|
|
|
}
|
|
|
|
</style>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link rel="alternate" type="applic
|
|
|
|
ation/rss+xml" title="RSS" href="/rss.xml">
|
2014-09-04 19:03:45 +02:00
|
|
|
<style type="text/css">
|
2014-09-27 14:45:03 +02:00
|
|
|
.yshortcuts a {border-bottom: none !important;}
|
|
|
|
@media screen and (max-width: 600px) {
|
|
|
|
table[class="container"] {
|
|
|
|
width: 100% !important;
|
|
|
|
}
|
|
|
|
}
|
2015-02-19 20:24:26 +01:00
|
|
|
/* Even comments should be preserved when the
|
|
|
|
keep_style_tags flag is set */
|
2014-09-04 19:03:45 +02:00
|
|
|
p {font-size:12px;}
|
|
|
|
</style>
|
2014-09-29 23:37:02 +02:00
|
|
|
<style type="text/css">h1 {
|
|
|
|
color: brown;
|
|
|
|
}
|
|
|
|
h2::after {
|
|
|
|
content: "";
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
font-size: 12px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|
2014-09-04 19:03:45 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2014-09-29 23:37:02 +02:00
|
|
|
<h1 style="color:brown">h1</h1>
|
2015-02-19 20:24:26 +01:00
|
|
|
<p style="font-size:12px"><a href="" style="{color:pink} :hover{col
|
|
|
|
or:purple}">html</a></p>
|
2014-09-04 19:03:45 +02:00
|
|
|
</body>
|
|
|
|
</html>
|
2015-02-19 20:24:26 +01:00
|
|
|
""".replace('col\nor', 'color').replace('applic\nation', 'application')
|
2014-09-04 19:03:45 +02:00
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
# for completeness, test it once without
|
|
|
|
with captured_output() as (out, err):
|
|
|
|
main([
|
|
|
|
'-f',
|
2014-09-28 20:11:22 +02:00
|
|
|
'premailer/tests/test-issue78.html',
|
2014-09-29 23:37:02 +02:00
|
|
|
'--external-style=premailer/tests/test-external-styles.css',
|
2014-09-04 19:03:45 +02:00
|
|
|
])
|
|
|
|
|
|
|
|
result_html = out.getvalue().strip()
|
|
|
|
expect_html = """
|
|
|
|
<html>
|
|
|
|
<head>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link rel="alternate" type="applic
|
|
|
|
ation/rss+xml" title="RSS" href="/rss.xml">
|
2014-09-27 14:45:03 +02:00
|
|
|
<style type="text/css">@media screen and (max-width: 600px) {
|
|
|
|
table[class="container"] {
|
|
|
|
width: 100% !important
|
|
|
|
}
|
|
|
|
}</style>
|
2014-09-29 23:37:02 +02:00
|
|
|
<style type="text/css">@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
font-size: 12px !important
|
|
|
|
}
|
|
|
|
}</style>
|
2014-09-04 19:03:45 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
2014-09-29 23:37:02 +02:00
|
|
|
<h1 style="color:brown">h1</h1>
|
2015-02-19 20:24:26 +01:00
|
|
|
<p style="font-size:12px"><a href="" style="{color:pink} :hover{co
|
|
|
|
lor:purple}">html</a></p>
|
2014-09-04 19:03:45 +02:00
|
|
|
</body>
|
|
|
|
</html>
|
2015-02-19 20:24:26 +01:00
|
|
|
""".replace('co\nlor', 'color').replace('applic\nation', 'application')
|
2014-09-04 19:03:45 +02:00
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
def test_multithreading(self):
|
|
|
|
"""The test tests thread safety of merge_styles function which employs
|
|
|
|
thread non-safe cssutils calls.
|
|
|
|
The test would fail if merge_styles would have not been thread-safe """
|
|
|
|
|
|
|
|
import threading
|
|
|
|
import logging
|
|
|
|
THREADS = 30
|
|
|
|
REPEATS = 100
|
|
|
|
|
|
|
|
class RepeatMergeStylesThread(threading.Thread):
|
2015-02-19 20:24:26 +01:00
|
|
|
"""The thread is instantiated by test and run multiple
|
|
|
|
times in parallel."""
|
2014-06-29 05:53:04 +02:00
|
|
|
exc = None
|
2014-09-24 04:54:16 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def __init__(self, old, new, class_):
|
|
|
|
"""The constructor just stores merge_styles parameters"""
|
|
|
|
super(RepeatMergeStylesThread, self).__init__()
|
|
|
|
self.old, self.new, self.class_ = old, new, class_
|
|
|
|
|
|
|
|
def run(self):
|
2015-02-19 20:24:26 +01:00
|
|
|
"""Calls merge_styles in a loop and sets exc attribute
|
|
|
|
if merge_styles raises an exception."""
|
2015-02-15 19:16:45 +01:00
|
|
|
for _ in range(0, REPEATS):
|
2014-06-29 05:53:04 +02:00
|
|
|
try:
|
|
|
|
merge_styles(self.old, self.new, self.class_)
|
2014-09-24 04:54:16 +02:00
|
|
|
except Exception as e:
|
2014-06-29 05:53:04 +02:00
|
|
|
logging.exception("Exception in thread %s", self.name)
|
|
|
|
self.exc = e
|
|
|
|
|
2015-02-15 19:16:45 +01:00
|
|
|
inline_style = 'background-color:#ffffff;'
|
2014-06-29 05:53:04 +02:00
|
|
|
new = 'background-color:#dddddd;'
|
|
|
|
class_ = ''
|
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
# start multiple threads concurrently; each
|
|
|
|
# calls merge_styles many times
|
2014-10-08 18:54:07 +02:00
|
|
|
threads = [
|
2015-02-19 20:24:26 +01:00
|
|
|
RepeatMergeStylesThread(
|
|
|
|
inline_style,
|
|
|
|
[csstext_to_pairs(new)],
|
|
|
|
[class_]
|
|
|
|
)
|
2015-02-15 19:16:45 +01:00
|
|
|
for _ in range(0, THREADS)
|
2014-10-08 18:54:07 +02:00
|
|
|
]
|
2014-06-29 05:53:04 +02:00
|
|
|
for t in threads:
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
# wait until all threads are done
|
|
|
|
for t in threads:
|
|
|
|
t.join()
|
|
|
|
|
|
|
|
# check if any thread raised exception while in merge_styles call
|
|
|
|
exceptions = [t.exc for t in threads if t.exc is not None]
|
|
|
|
eq_(exceptions, [])
|
|
|
|
|
|
|
|
def test_external_links(self):
|
|
|
|
"""Test loading stylesheets via link tags"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:red; }
|
|
|
|
h3 { color:yellow; }
|
|
|
|
</style>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link href="premailer/tests/test-external-links.css" rel="style
|
|
|
|
sheet" type="text/css">
|
|
|
|
<link rel="alternate" type="applic
|
|
|
|
ation/rss+xml" title="RSS" href="/rss.xml">
|
2014-06-29 05:53:04 +02:00
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:orange; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
<h3>Test</h3>
|
|
|
|
<a href="#">Link</a>
|
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace(
|
|
|
|
'applic\naction', 'application'
|
|
|
|
).replace('style\nsheet', 'stylesheet')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">a:hover {color:purple !important}</style>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link rel="alternate" type="applic
|
|
|
|
ation/rss+xml" title="RSS" href="/rss.xml">
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:orange">Hello</h1>
|
|
|
|
<h2 style="color:green">World</h2>
|
|
|
|
<h3 style="color:yellow">Test</h3>
|
|
|
|
<a href="#" style="color:pink">Link</a>
|
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('applic\naction', 'application')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
strip_important=False
|
|
|
|
)
|
2014-06-29 05:53:04 +02:00
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-08-22 00:13:03 +02:00
|
|
|
def test_external_links_unfindable(self):
|
|
|
|
"""Test loading stylesheets that can't be found"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:red; }
|
|
|
|
h3 { color:yellow; }
|
|
|
|
</style>
|
|
|
|
<link href="premailer/xxxx.css" rel="stylesheet" type="text/css">
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:orange; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
<h3>Test</h3>
|
|
|
|
<a href="#">Link</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
strip_important=False
|
|
|
|
)
|
|
|
|
assert_raises(
|
|
|
|
ExternalNotFoundError,
|
|
|
|
p.transform,
|
|
|
|
)
|
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
def test_external_styles_and_links(self):
|
2015-02-19 20:24:26 +01:00
|
|
|
"""Test loading stylesheets via both the 'external_styles'
|
|
|
|
argument and link tags"""
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
2014-09-29 19:16:53 +02:00
|
|
|
<link href="test-external-links.css" rel="stylesheet" type="text/css">
|
2014-06-29 05:53:04 +02:00
|
|
|
<style type="text/css">
|
|
|
|
h1 { color: red; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
2014-08-08 19:08:58 +02:00
|
|
|
<h2>Hello</h2>
|
|
|
|
<a href="">Hello</a>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">a:hover {color:purple !important}</style>
|
2015-02-19 20:24:26 +01:00
|
|
|
<style type="text/css">h2::after {cont
|
|
|
|
ent:"" !important;display:block !important}
|
2014-08-08 19:08:58 +02:00
|
|
|
@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
font-size: 12px !important
|
|
|
|
}
|
|
|
|
}</style>
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
2014-08-08 19:08:58 +02:00
|
|
|
<h2 style="color:green">Hello</h2>
|
|
|
|
<a href="" style="color:pink">Hello</a>
|
2014-06-29 05:53:04 +02:00
|
|
|
</body>
|
2015-02-19 20:24:26 +01:00
|
|
|
</html>""".replace('cont\nent', 'content')
|
2014-06-29 05:53:04 +02:00
|
|
|
|
2014-10-08 18:55:30 +02:00
|
|
|
p = Premailer(
|
|
|
|
html,
|
2014-06-29 05:53:04 +02:00
|
|
|
strip_important=False,
|
2014-10-08 18:55:30 +02:00
|
|
|
external_styles='test-external-styles.css',
|
|
|
|
base_path='premailer/tests/')
|
2014-06-29 05:53:04 +02:00
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2016-03-15 19:13:53 +01:00
|
|
|
@mock.patch('premailer.premailer.requests')
|
|
|
|
def test_load_external_url(self, mocked_requests):
|
2014-09-24 07:07:50 +02:00
|
|
|
'Test premailer.premailer.Premailer._load_external_url'
|
2016-03-15 19:13:53 +01:00
|
|
|
faux_response = 'This is not a response'
|
2014-10-08 19:19:37 +02:00
|
|
|
faux_uri = 'https://example.com/site.css'
|
2016-03-15 19:13:53 +01:00
|
|
|
mocked_requests.get.return_value = MockResponse(faux_response)
|
2014-10-08 19:19:37 +02:00
|
|
|
p = premailer.premailer.Premailer('<p>A paragraph</p>')
|
|
|
|
r = p._load_external_url(faux_uri)
|
|
|
|
|
2016-03-15 19:13:53 +01:00
|
|
|
mocked_requests.get.assert_called_once_with(faux_uri)
|
|
|
|
eq_(faux_response, r)
|
2014-09-24 07:07:50 +02:00
|
|
|
|
2014-10-17 02:16:28 +02:00
|
|
|
def test_css_text(self):
|
|
|
|
"""Test handling css_text passed as a string"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>Hello</h2>
|
|
|
|
<a href="">Hello</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
color: black !important
|
|
|
|
}
|
|
|
|
}</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
<h2 style="color:green">Hello</h2>
|
|
|
|
<a href="" style="color:pink">Hello</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
css_text = """
|
|
|
|
h1 {
|
|
|
|
color: brown;
|
|
|
|
}
|
|
|
|
h2 {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
a {
|
|
|
|
color: pink;
|
|
|
|
}
|
|
|
|
@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
strip_important=False,
|
|
|
|
css_text=[css_text])
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_css_text_with_only_body_present(self):
|
2015-02-19 20:24:26 +01:00
|
|
|
"""Test handling css_text passed as a string when no <html> or
|
|
|
|
<head> is present"""
|
2014-10-17 02:16:28 +02:00
|
|
|
|
|
|
|
html = """<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>Hello</h2>
|
|
|
|
<a href="">Hello</a>
|
|
|
|
</body>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<style type="text/css">@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
color: black !important
|
|
|
|
}
|
|
|
|
}</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
<h2 style="color:green">Hello</h2>
|
|
|
|
<a href="" style="color:pink">Hello</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
css_text = """
|
|
|
|
h1 {
|
|
|
|
color: brown;
|
|
|
|
}
|
|
|
|
h2 {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
a {
|
|
|
|
color: pink;
|
|
|
|
}
|
|
|
|
@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
strip_important=False,
|
|
|
|
css_text=css_text)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2015-05-12 03:14:27 +02:00
|
|
|
def test_css_disable_leftover_css(self):
|
|
|
|
"""Test handling css_text passed as a string when no <html> or
|
|
|
|
<head> is present"""
|
|
|
|
|
|
|
|
html = """<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>Hello</h2>
|
|
|
|
<a href="">Hello</a>
|
|
|
|
</body>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
<h2 style="color:green">Hello</h2>
|
|
|
|
<a href="" style="color:pink">Hello</a>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
css_text = """
|
|
|
|
h1 {
|
|
|
|
color: brown;
|
|
|
|
}
|
|
|
|
h2 {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
a {
|
|
|
|
color: pink;
|
|
|
|
}
|
|
|
|
@media all and (max-width: 320px) {
|
|
|
|
h1 {
|
|
|
|
color: black;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
strip_important=False,
|
|
|
|
css_text=css_text,
|
|
|
|
disable_leftover_css=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-09-24 18:31:16 +02:00
|
|
|
@staticmethod
|
|
|
|
def mocked_urlopen(url):
|
|
|
|
'The standard "response" from the "server".'
|
|
|
|
retval = ''
|
|
|
|
if 'style1.css' in url:
|
|
|
|
retval = "h1 { color: brown }"
|
|
|
|
elif 'style2.css' in url:
|
|
|
|
retval = "h2 { color: pink }"
|
|
|
|
elif 'style3.css' in url:
|
|
|
|
retval = "h3 { color: red }"
|
|
|
|
return retval
|
|
|
|
|
2014-09-24 20:36:09 +02:00
|
|
|
@mock.patch.object(Premailer, '_load_external_url')
|
|
|
|
def test_external_styles_on_http(self, mocked_pleu):
|
2014-06-29 05:53:04 +02:00
|
|
|
"""Test loading styles that are genuinely external"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link href="https://www.com/style1.css" rel="stylesheet">
|
|
|
|
<link href="//www.com/style2.css" rel="stylesheet">
|
|
|
|
<link href="//www.com/style3.css" rel="stylesheet">
|
2014-06-29 05:53:04 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
<h3>World</h3>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-09-24 20:36:09 +02:00
|
|
|
mocked_pleu.side_effect = self.mocked_urlopen
|
|
|
|
p = Premailer(html)
|
|
|
|
result_html = p.transform()
|
2014-06-29 05:53:04 +02:00
|
|
|
|
2014-09-24 20:36:09 +02:00
|
|
|
# Expected values are tuples of the positional values (as another
|
|
|
|
# tuple) and the ketword arguments (which are all null), hence the
|
|
|
|
# following Lisp-like explosion of brackets and commas.
|
2014-10-08 19:02:26 +02:00
|
|
|
expected_args = [(('https://www.com/style1.css',),),
|
|
|
|
(('http://www.com/style2.css',),),
|
|
|
|
(('http://www.com/style3.css',),)]
|
2014-10-08 19:03:52 +02:00
|
|
|
eq_(expected_args, mocked_pleu.call_args_list)
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
<h2 style="color:pink">World</h2>
|
|
|
|
<h3 style="color:red">World</h3>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-09-24 20:36:09 +02:00
|
|
|
@mock.patch.object(Premailer, '_load_external_url')
|
|
|
|
def test_external_styles_on_https(self, mocked_pleu):
|
2014-08-22 00:13:03 +02:00
|
|
|
"""Test loading styles that are genuinely external"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
2015-02-19 20:24:26 +01:00
|
|
|
<link href="https://www.com/style1.css" rel="stylesheet">
|
|
|
|
<link href="//www.com/style2.css" rel="stylesheet">
|
|
|
|
<link href="/style3.css" rel="stylesheet">
|
2014-08-22 00:13:03 +02:00
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
<h3>World</h3>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
2014-09-24 20:36:09 +02:00
|
|
|
mocked_pleu.side_effect = self.mocked_urlopen
|
|
|
|
p = Premailer(html, base_url='https://www.peterbe.com')
|
|
|
|
result_html = p.transform()
|
|
|
|
|
2014-10-08 19:02:26 +02:00
|
|
|
expected_args = [(('https://www.com/style1.css',),),
|
|
|
|
(('https://www.com/style2.css',),),
|
|
|
|
(('https://www.peterbe.com/style3.css',),)]
|
2014-12-25 03:30:30 +01:00
|
|
|
eq_(expected_args, mocked_pleu.call_args_list)
|
2014-08-22 00:13:03 +02:00
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
<h2 style="color:pink">World</h2>
|
|
|
|
<h3 style="color:red">World</h3>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
2014-09-24 20:36:09 +02:00
|
|
|
@mock.patch.object(Premailer, '_load_external_url')
|
|
|
|
def test_external_styles_with_base_url(self, mocked_pleu):
|
2014-06-29 05:53:04 +02:00
|
|
|
"""Test loading styles that are genuinely external if you use
|
|
|
|
the base_url"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<link href="style.css" rel="stylesheet" type="text/css">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2014-09-24 20:36:09 +02:00
|
|
|
mocked_pleu.return_value = "h1 { color: brown }"
|
|
|
|
p = Premailer(html, base_url='http://www.peterbe.com/')
|
|
|
|
result_html = p.transform()
|
2014-10-08 19:02:26 +02:00
|
|
|
expected_args = [(('http://www.peterbe.com/style.css',),), ]
|
2014-12-25 03:30:30 +01:00
|
|
|
eq_(expected_args, mocked_pleu.call_args_list)
|
2014-06-29 05:53:04 +02:00
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:brown">Hello</h1>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_disabled_validator(self):
|
|
|
|
"""test disabled_validator"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { fo:bar; }
|
|
|
|
strong {
|
|
|
|
color:baz;
|
|
|
|
text-decoration:none;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="fo:bar">Hi!</h1>
|
|
|
|
<p><strong style="color:baz; text-decoration:none">Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_comments_in_media_queries(self):
|
|
|
|
"""CSS comments inside a media query block should not be a problem"""
|
|
|
|
html = """<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<title>Document</title>
|
|
|
|
<style>
|
|
|
|
@media screen {
|
|
|
|
/* comment */
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body></body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
ok_('/* comment */' in result_html)
|
|
|
|
|
|
|
|
def test_fontface_selectors_with_no_selectortext(self):
|
|
|
|
"""
|
|
|
|
@font-face selectors are weird.
|
|
|
|
This is a fix for https://github.com/peterbe/premailer/issues/71
|
|
|
|
"""
|
|
|
|
html = """<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<title>Document</title>
|
|
|
|
<style>
|
|
|
|
@font-face {
|
|
|
|
font-family: 'Garamond';
|
|
|
|
src:
|
|
|
|
local('Garamond'),
|
|
|
|
local('Garamond-Regular'),
|
2015-02-19 20:24:26 +01:00
|
|
|
url('Garamond.ttf') format('truetype');
|
2014-06-29 05:53:04 +02:00
|
|
|
font-weight: normal;
|
|
|
|
font-style: normal;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body></body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
p.transform() # it should just work
|
|
|
|
|
|
|
|
def test_keyframe_selectors(self):
|
|
|
|
"""
|
|
|
|
keyframes shouldn't be a problem.
|
|
|
|
"""
|
|
|
|
html = """<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<title>Document</title>
|
|
|
|
<style>
|
|
|
|
@keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Firefox */
|
|
|
|
@-moz-keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Safari and Chrome */
|
|
|
|
@-webkit-keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Internet Explorer */
|
|
|
|
@-ms-keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Opera */
|
|
|
|
@-o-keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body></body>
|
|
|
|
</html>"""
|
2014-06-26 19:14:49 +02:00
|
|
|
|
2014-06-29 05:53:04 +02:00
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
p.transform() # it should just work
|
2014-12-11 18:36:33 +01:00
|
|
|
|
2015-06-10 18:40:35 +02:00
|
|
|
def test_capture_cssutils_logging(self):
|
|
|
|
"""you can capture all the warnings, errors etc. from cssutils
|
|
|
|
with your own logging. """
|
|
|
|
html = """<!doctype html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<title>Document</title>
|
|
|
|
<style>
|
|
|
|
@keyframes fadein {
|
|
|
|
from { opacity: 0; }
|
|
|
|
to { opacity: 1; }
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body></body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
mylog = StringIO()
|
|
|
|
myhandler = logging.StreamHandler(mylog)
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
cssutils_logging_handler=myhandler,
|
|
|
|
)
|
|
|
|
p.transform() # it should work
|
|
|
|
eq_(
|
|
|
|
mylog.getvalue(),
|
|
|
|
'CSSStylesheet: Unknown @rule found. [2:13: @keyframes]\n'
|
|
|
|
)
|
|
|
|
|
|
|
|
# only log errors now
|
|
|
|
mylog = StringIO()
|
|
|
|
myhandler = logging.StreamHandler(mylog)
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
cssutils_logging_handler=myhandler,
|
|
|
|
cssutils_logging_level=logging.ERROR,
|
|
|
|
)
|
|
|
|
p.transform() # it should work
|
|
|
|
eq_(mylog.getvalue(), '')
|
|
|
|
|
2014-12-11 18:36:33 +01:00
|
|
|
def test_type_test(self):
|
2014-12-25 03:41:44 +01:00
|
|
|
"""test the correct type is returned"""
|
2014-12-11 18:36:33 +01:00
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1, h2 { color:red; }
|
|
|
|
strong {
|
|
|
|
text-decoration:none
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hi!</h1>
|
|
|
|
<p><strong>Yes!</strong></p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result = p.transform()
|
2014-12-25 03:30:30 +01:00
|
|
|
eq_(type(result), type(""))
|
2014-12-11 18:36:33 +01:00
|
|
|
|
|
|
|
html = fromstring(html)
|
|
|
|
etree_type = type(html)
|
|
|
|
|
|
|
|
p = Premailer(html)
|
|
|
|
result = p.transform()
|
2014-12-25 03:30:30 +01:00
|
|
|
ok_(type(result) != etree_type)
|
2014-12-26 03:44:13 +01:00
|
|
|
|
|
|
|
def test_ignore_some_inline_stylesheets(self):
|
|
|
|
"""test that it's possible to put a `data-premailer="ignore"`
|
|
|
|
attribute on a <style> tag and it gets left alone (except that
|
|
|
|
the attribute gets removed)"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:red; }
|
|
|
|
</style>
|
|
|
|
<style type="text/css" data-premailer="ignore">
|
|
|
|
h1 { color:blue; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">
|
|
|
|
h1 { color:blue; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:red">Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
result_html = p.transform()
|
2015-02-19 20:24:26 +01:00
|
|
|
compare_html(expect_html, result_html)
|
2014-12-26 03:44:13 +01:00
|
|
|
|
|
|
|
@mock.patch('premailer.premailer.warnings')
|
|
|
|
def test_ignore_some_incorrectly(self, warnings_mock):
|
|
|
|
"""You can put `data-premailer="ignore"` but if the attribute value
|
|
|
|
is something we don't recognize you get a warning"""
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css" data-premailer="blah">
|
|
|
|
h1 { color:blue; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:blue">Hello</h1>
|
|
|
|
<h2>World</h2>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
warnings_mock.warn.assert_called_with(
|
|
|
|
"Unrecognized data-premailer attribute ('blah')"
|
|
|
|
)
|
|
|
|
|
|
|
|
compare_html(expect_html, result_html)
|
|
|
|
|
|
|
|
def test_ignore_some_external_stylesheets(self):
|
|
|
|
"""test that it's possible to put a `data-premailer="ignore"`
|
|
|
|
attribute on a <link> tag and it gets left alone (except that
|
|
|
|
the attribute gets removed)"""
|
|
|
|
|
|
|
|
# Know thy fixtures!
|
|
|
|
# The test-external-links.css has a `h1{color:blue}`
|
|
|
|
# And the test-external-styles.css has a `h1{color:brown}`
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<link href="premailer/tests/test-external-links.css"
|
|
|
|
rel="stylesheet" type="text/css">
|
|
|
|
<link data-premailer="ignore"
|
|
|
|
href="premailer/tests/test-external-styles.css"
|
|
|
|
rel="stylesheet" type="text/css">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Hello</h1>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
# Note that the `test-external-links.css` gets converted to a inline
|
|
|
|
# style sheet.
|
|
|
|
expect_html = """<html>
|
2015-02-19 20:24:26 +01:00
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style type="text/css">a:hover {color:purple}</style>
|
|
|
|
<link href="premailer/tests/test-external-styles.css" rel="style
|
|
|
|
sheet" type="text/css">
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="color:blue">Hello</h1>
|
|
|
|
</body>
|
|
|
|
</html>""".replace('style\nsheet', 'stylesheet')
|
2014-12-26 03:44:13 +01:00
|
|
|
|
|
|
|
p = Premailer(html, disable_validation=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|
2015-02-19 20:24:26 +01:00
|
|
|
|
2015-02-17 23:41:41 +01:00
|
|
|
def test_turnoff_cache_works_as_expected(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
.color {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
div.example {
|
|
|
|
font-size: 10px;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="color example"></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="color example" style="color:green; font-size:10px"></div>
|
2015-02-17 23:41:41 +01:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, cache_css_parsing=False)
|
|
|
|
self.assertFalse(p.cache_css_parsing)
|
|
|
|
# run one time first
|
|
|
|
p.transform()
|
|
|
|
result_html = p.transform()
|
|
|
|
|
2015-02-19 20:24:26 +01:00
|
|
|
compare_html(expect_html, result_html)
|
2015-05-08 18:37:52 +02:00
|
|
|
|
|
|
|
def test_links_without_protocol(self):
|
|
|
|
"""If you the base URL is set to https://example.com and your html
|
|
|
|
contains <img src="//otherdomain.com/">... then the URL to point to
|
|
|
|
is "https://otherdomain.com/" not "https://example.com/file.css"
|
|
|
|
"""
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="//example.com">
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<img src="{protocol}://example.com">
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, base_url='https://www.peterbe.com')
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html.format(protocol="https"), result_html)
|
|
|
|
|
|
|
|
p = Premailer(html, base_url='http://www.peterbe.com')
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html.format(protocol="http"), result_html)
|
|
|
|
|
|
|
|
# Because you can't set a base_url without a full protocol
|
|
|
|
p = Premailer(html, base_url='www.peterbe.com')
|
|
|
|
assert_raises(ValueError, p.transform)
|
2015-08-27 22:38:40 +02:00
|
|
|
|
|
|
|
def test_align_float_images(self):
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
<style>
|
|
|
|
.floatright {
|
|
|
|
float: right;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p><img src="/images/left.jpg" style="float: left"> text
|
2016-06-07 15:59:45 +02:00
|
|
|
<img src="/r.png" class="floatright"> text
|
2015-08-27 22:38:40 +02:00
|
|
|
<img src="/images/nofloat.gif"> text
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
2016-06-07 15:59:45 +02:00
|
|
|
<head>
|
|
|
|
<title>Title</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<p><img src="/images/left.jpg" style="float: left" align="left"> text
|
|
|
|
<img src="/r.png" class="floatright" style="float:right" align="right"> text
|
|
|
|
<img src="/images/nofloat.gif"> text
|
|
|
|
</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
2015-08-27 22:38:40 +02:00
|
|
|
|
|
|
|
p = Premailer(html, align_floating_images=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|
2015-09-09 22:37:38 +02:00
|
|
|
|
|
|
|
def test_remove_unset_properties(self):
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
div {
|
|
|
|
color: green;
|
|
|
|
}
|
|
|
|
span {
|
|
|
|
color: blue;
|
|
|
|
}
|
|
|
|
span.nocolor {
|
|
|
|
color: unset;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="color"><span class="nocolor"></span></div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body>
|
2016-06-07 15:59:45 +02:00
|
|
|
<div class="color" style="color:green"><span class="nocolor"></span>
|
|
|
|
</div>
|
2015-09-09 22:37:38 +02:00
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
p = Premailer(html, remove_unset_properties=True)
|
|
|
|
self.assertTrue(p.remove_unset_properties)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|
2015-09-23 04:45:57 +02:00
|
|
|
|
|
|
|
def test_six_color(self):
|
|
|
|
r = Premailer.six_color('#cde')
|
|
|
|
e = '#ccddee'
|
|
|
|
self.assertEqual(e, r)
|
|
|
|
|
|
|
|
def test_3_digit_color_expand(self):
|
|
|
|
'Are 3-digit color values expanded into 6-digits for IBM Notes'
|
|
|
|
html = """<html>
|
|
|
|
<style>
|
|
|
|
body {background-color: #fe5;}
|
|
|
|
p {background-color: #123456;}
|
|
|
|
h1 {color: #f0df0d;}
|
|
|
|
</style>
|
|
|
|
<body>
|
|
|
|
<h1>color test</h1>
|
|
|
|
<p>
|
|
|
|
This is a test of color handling.
|
|
|
|
</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
</head>
|
|
|
|
<body style="background-color:#fe5" bgcolor="#ffee55">
|
|
|
|
<h1 style="color:#f0df0d">color test</h1>
|
|
|
|
<p style="background-color:#123456" bgcolor="#123456">
|
|
|
|
This is a test of color handling.
|
|
|
|
</p>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
p = Premailer(html, remove_unset_properties=True)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|
2016-04-12 03:04:56 +02:00
|
|
|
|
|
|
|
def test_inline_important(self):
|
|
|
|
'Are !important tags preserved inline.'
|
|
|
|
|
|
|
|
html = """<html>
|
|
|
|
<head>
|
|
|
|
<title></title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<style type="text/css">.something { display:none !important; }</style>
|
|
|
|
<div class="something">blah</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
expect_html = """<html>
|
|
|
|
<head>
|
|
|
|
<title></title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<style type="text/css">.something { display:none !important; }</style>
|
|
|
|
<div class="something" style="display:none !important">blah</div>
|
|
|
|
</body>
|
|
|
|
</html>"""
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
remove_classes=False,
|
|
|
|
keep_style_tags=True,
|
|
|
|
strip_important=False
|
|
|
|
)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|
2017-07-25 18:07:38 +02:00
|
|
|
|
|
|
|
def test_pseudo_selectors_without_selector(self):
|
|
|
|
"""Happens when you have pseudo selectors without an actual selector.
|
|
|
|
Which means it's not possible to find it in the DOM.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
<style>
|
|
|
|
:before{box-sizing:inherit}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
Semantic-UI uses this in its normalizer.
|
|
|
|
|
|
|
|
Original issue: https://github.com/peterbe/premailer/issues/184
|
|
|
|
"""
|
|
|
|
|
|
|
|
html = """
|
|
|
|
<html>
|
|
|
|
<style>
|
|
|
|
*,:after,:before{box-sizing:inherit}
|
|
|
|
h1{ border: 1px solid blue}
|
|
|
|
h1:hover {border: 1px solid green}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
<h1>Hey</h1>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
|
|
|
|
expect_html = """
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<style>
|
|
|
|
*,:after,:before{box-sizing:inherit}
|
|
|
|
h1{ border: 1px solid blue}
|
|
|
|
h1:hover {border: 1px solid green}
|
|
|
|
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1 style="{border:1px solid blue} :hover{border:1px solid green}">Hey</h1>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
"""
|
|
|
|
p = Premailer(
|
|
|
|
html,
|
|
|
|
exclude_pseudoclasses=False,
|
|
|
|
keep_style_tags=True,
|
|
|
|
)
|
|
|
|
result_html = p.transform()
|
|
|
|
compare_html(expect_html, result_html)
|