171 lines
6.5 KiB
Python
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
|