This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
python-emails/emails/loader/cssinliner.py

171 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
# adapted from https://github.com/kgn/cssutils/blob/master/examples/style.py
from __future__ import unicode_literals, print_function
import logging
from cssutils.css import CSSStyleSheet, CSSStyleDeclaration, CSSStyleRule
from cssutils import CSSParser
from lxml import etree
from emails.compat import to_unicode, string_types
import emails
# Workaround the missing python3-cssselect package
# If no system-installed cssselect library found, use one from our distribution
try:
import cssselect
except ImportError:
import sys, os.path
my_packages = os.path.dirname(emails.packages.__file__)
sys.path.insert(0, my_packages)
import cssselect
from lxml.cssselect import CSSSelector, ExpressionError
class CSSInliner:
NONVISUAL_TAGS = ['html', 'head', 'title', 'meta', 'link', 'script']
DEBUG = False
def __init__(self, base_url=None, css=None):
self.stylesheet = CSSStyleSheet(href=base_url)
self.base_url = base_url
if css:
self.add_css(css)
def add_css(self, css, href=None):
if isinstance(css, string_types):
css = CSSParser().parseString(css, href=href) # Распарсим файл
for rule in css:
self.stylesheet.add(rule)
def log(self, level, *msg):
if self.DEBUG:
print(('%s- %s' % (level * '\t ', ' '.join((to_unicode(m or '') for m in msg)))))
def styleattribute(self, element):
"""returns css.CSSStyleDeclaration of inline styles, for html: @style"""
cssText = element.get('style')
if cssText:
try:
return CSSStyleDeclaration(cssText=cssText)
except Exception as e:
# Sometimes here's error like "COLOR: ;"
logging.exception('Exception in styleattribute %s', cssText)
return None
else:
return None
def getView(self, document, sheet, media='all', name=None, styleCallback=None):
"""
document
a DOM document, currently an lxml HTML document
sheet
a CSS StyleSheet object, currently cssutils sheet
media: optional
TODO: view for which media it should be
name: optional
TODO: names of sheets only
styleCallback: optional
should return css.CSSStyleDeclaration of inline styles, for html
a style declaration for ``element@style``. Gets one parameter
``element`` which is the relevant DOMElement
returns style view
a dict of {DOMElement: css.CSSStyleDeclaration} for html
"""
styleCallback = styleCallback or self.styleattribute
_unmergable_rules = CSSStyleSheet()
view = {}
specificities = {} # needed temporarily
# TODO: filter rules simpler?, add @media
rules = (rule for rule in sheet if rule.type == rule.STYLE_RULE)
for rule in rules:
for selector in rule.selectorList:
self.log(0, 'SELECTOR', selector.selectorText)
# TODO: make this a callback to be able to use other stuff than lxml
try:
cssselector = CSSSelector(selector.selectorText)
except (ExpressionError, NotImplementedError) as e:
_unmergable_rules.add(CSSStyleRule(selectorText=selector.selectorText,
style=rule.style))
continue
matching = cssselector.evaluate(document)
for element in matching:
if element.tag in self.NONVISUAL_TAGS:
continue
# add styles for all matching DOM elements
self.log(1, 'ELEMENT', id(element), element.text)
if element not in view:
# add initial empty style declatation
view[element] = CSSStyleDeclaration()
specificities[element] = {}
# and add inline @style if present
inlinestyle = styleCallback(element)
if inlinestyle:
for p in inlinestyle:
# set inline style specificity
view[element].setProperty(p)
specificities[element][p.name] = (1, 0, 0, 0)
for p in rule.style:
# update style declaration
if p not in view[element]:
# setProperty needs a new Property object and
# MUST NOT reuse the existing Property
# which would be the same for all elements!
# see Issue #23
view[element].setProperty(p.name, p.value, p.priority)
specificities[element][p.name] = selector.specificity
self.log(2, view[element].getProperty('color'))
else:
self.log(2, view[element].getProperty('color'))
sameprio = (p.priority ==
view[element].getPropertyPriority(p.name))
if not sameprio and bool(p.priority) or (
sameprio and selector.specificity >=
specificities[element][p.name]):
# later, more specific or higher prio
view[element].setProperty(p.name, p.value, p.priority)
_unmergable_css = _unmergable_rules.cssText
if _unmergable_css:
e = etree.Element('style')
e.text = to_unicode(_unmergable_css, 'utf-8')
body = document.find('body') or document
body.insert(0, e) # add <style> right into body
return view
def transform(self, html):
if isinstance(html, string_types):
html = etree.HTML(html, parser=etree.HTMLParser())
view = self.getView(html, self.stylesheet)
# - add style into @style attribute
for element, style in list(view.items()):
v = style.getCssText(separator='')
element.set('style', v)
return html
transform_html = transform # compatibility