%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/oeb/polish/ |
| Current File : //usr/lib/calibre/calibre/ebooks/oeb/polish/cascade.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from collections import defaultdict, namedtuple
from functools import partial
from itertools import count
from operator import itemgetter
import re
from css_parser.css import CSSStyleSheet, CSSRule, Property
from css_selectors import Select, INAPPROPRIATE_PSEUDO_CLASSES, SelectorError
from calibre import as_unicode
from calibre.ebooks.css_transform_rules import all_properties
from calibre.ebooks.oeb.base import OEB_STYLES, XHTML, css_text
from calibre.ebooks.oeb.normalize_css import normalizers, DEFAULTS
from calibre.ebooks.oeb.stylizer import media_ok, INHERITED
from tinycss.fonts3 import serialize_font_family, parse_font_family
from polyglot.builtins import iteritems, itervalues
_html_css_stylesheet = None
def html_css_stylesheet(container):
global _html_css_stylesheet
if _html_css_stylesheet is None:
data = P('templates/html.css', data=True).decode('utf-8')
_html_css_stylesheet = container.parse_css(data, 'user-agent.css')
return _html_css_stylesheet
def media_allowed(media):
if not media or not media.mediaText:
return True
return media_ok(media.mediaText)
def iterrules(container, sheet_name, rules=None, media_rule_ok=media_allowed, rule_index_counter=None, rule_type=None, importing=None):
''' Iterate over all style rules in the specified sheet. Import and Media rules are
automatically resolved. Yields (rule, sheet_name, rule_number).
:param rules: List of CSSRules or a CSSStyleSheet instance or None in which case it is read from container using sheet_name
:param sheet_name: The name of the sheet in the container (in case of inline style sheets, the name of the html file)
:param media_rule_ok: A function to test if a @media rule is allowed
:param rule_index_counter: A counter object, rule numbers will be calculated by incrementing the counter.
:param rule_type: Only yield rules of this type, where type is a string type name, see css_parser.css.CSSRule for the names (
by default all rules are yielded)
:return: (CSSRule object, the name of the sheet from which it comes, rule index - a monotonically increasing number)
'''
rule_index_counter = rule_index_counter or count()
if importing is None:
importing = set()
importing.add(sheet_name)
riter = partial(iterrules, container, rule_index_counter=rule_index_counter, media_rule_ok=media_rule_ok, rule_type=rule_type, importing=importing)
if rules is None:
rules = container.parsed(sheet_name)
if rule_type is not None:
rule_type = getattr(CSSRule, rule_type)
for rule in rules:
if rule.type == CSSRule.IMPORT_RULE:
if media_rule_ok(rule.media):
name = container.href_to_name(rule.href, sheet_name)
if container.has_name(name):
if name in importing:
container.log.error(f'Recursive import of {name} from {sheet_name}, ignoring')
else:
csheet = container.parsed(name)
if isinstance(csheet, CSSStyleSheet):
yield from riter(name, rules=csheet)
elif rule.type == CSSRule.MEDIA_RULE:
if media_rule_ok(rule.media):
yield from riter(sheet_name, rules=rule.cssRules)
elif rule_type is None or rule.type == rule_type:
num = next(rule_index_counter)
yield rule, sheet_name, num
importing.discard(sheet_name)
StyleDeclaration = namedtuple('StyleDeclaration', 'index declaration pseudo_element')
Specificity = namedtuple('Specificity', 'is_style num_id num_class num_elem rule_index')
def specificity(rule_index, selector, is_style=0):
s = selector.specificity
return Specificity(is_style, s[1], s[2], s[3], rule_index)
def iterdeclaration(decl):
for p in all_properties(decl):
n = normalizers.get(p.name)
if n is None:
yield p
else:
for k, v in iteritems(n(p.name, p.propertyValue)):
yield Property(k, v, p.literalpriority)
class Values(tuple):
''' A tuple of `css_parser.css.Value ` (and its subclasses) objects. Also has a
`sheet_name` attribute that is the canonical name relative to which URLs
for this property should be resolved. '''
def __new__(typ, pv, sheet_name=None, priority=''):
ans = tuple.__new__(typ, pv)
ans.sheet_name = sheet_name
ans.is_important = priority == 'important'
return ans
@property
def cssText(self):
' This will return either a string or a tuple of strings '
if len(self) == 1:
return css_text(self[0])
return tuple(css_text(x) for x in self)
def normalize_style_declaration(decl, sheet_name):
ans = {}
for prop in iterdeclaration(decl):
if prop.name == 'font-family':
# Needed because of https://bitbucket.org/cthedot/cssutils/issues/66/incorrect-handling-of-spaces-in-font
prop.propertyValue.cssText = serialize_font_family(parse_font_family(css_text(prop.propertyValue)))
ans[prop.name] = Values(prop.propertyValue, sheet_name, prop.priority)
return ans
def resolve_declarations(decls):
property_names = set()
for d in decls:
property_names |= set(d.declaration)
ans = {}
for name in property_names:
first_val = None
for decl in decls:
x = decl.declaration.get(name)
if x is not None:
if x.is_important:
first_val = x
break
if first_val is None:
first_val = x
ans[name] = first_val
return ans
def resolve_pseudo_declarations(decls):
groups = defaultdict(list)
for d in decls:
groups[d.pseudo_element].append(d)
return {k:resolve_declarations(v) for k, v in iteritems(groups)}
def resolve_styles(container, name, select=None, sheet_callback=None):
root = container.parsed(name)
select = select or Select(root, ignore_inappropriate_pseudo_classes=True)
style_map = defaultdict(list)
pseudo_style_map = defaultdict(list)
rule_index_counter = count()
pseudo_pat = re.compile(':{1,2}(%s)' % ('|'.join(INAPPROPRIATE_PSEUDO_CLASSES)), re.I)
def process_sheet(sheet, sheet_name):
if sheet_callback is not None:
sheet_callback(sheet, sheet_name)
for rule, sheet_name, rule_index in iterrules(container, sheet_name, rules=sheet, rule_index_counter=rule_index_counter, rule_type='STYLE_RULE'):
for selector in rule.selectorList:
text = selector.selectorText
try:
matches = tuple(select(text))
except SelectorError as err:
container.log.error(f'Ignoring CSS rule with invalid selector: {text!r} ({as_unicode(err)})')
continue
m = pseudo_pat.search(text)
style = normalize_style_declaration(rule.style, sheet_name)
if m is None:
for elem in matches:
style_map[elem].append(StyleDeclaration(specificity(rule_index, selector), style, None))
else:
for elem in matches:
pseudo_style_map[elem].append(StyleDeclaration(specificity(rule_index, selector), style, m.group(1)))
process_sheet(html_css_stylesheet(container), 'user-agent.css')
for elem in root.iterdescendants(XHTML('style'), XHTML('link')):
if elem.tag.lower().endswith('style'):
if not elem.text:
continue
sheet = container.parse_css(elem.text)
sheet_name = name
else:
if (elem.get('type') or 'text/css').lower() not in OEB_STYLES or \
(elem.get('rel') or 'stylesheet').lower() != 'stylesheet' or \
not media_ok(elem.get('media')):
continue
href = elem.get('href')
if not href:
continue
sheet_name = container.href_to_name(href, name)
if not container.has_name(sheet_name):
continue
sheet = container.parsed(sheet_name)
if not isinstance(sheet, CSSStyleSheet):
continue
process_sheet(sheet, sheet_name)
for elem in root.xpath('//*[@style]'):
text = elem.get('style')
if text:
style = container.parse_css(text, is_declaration=True)
style_map[elem].append(StyleDeclaration(Specificity(1, 0, 0, 0, 0), normalize_style_declaration(style, name), None))
for l in (style_map, pseudo_style_map):
for x in itervalues(l):
x.sort(key=itemgetter(0), reverse=True)
style_map = {elem:resolve_declarations(x) for elem, x in iteritems(style_map)}
pseudo_style_map = {elem:resolve_pseudo_declarations(x) for elem, x in iteritems(pseudo_style_map)}
return partial(resolve_property, style_map), partial(resolve_pseudo_property, style_map, pseudo_style_map), select
_defvals = None
def defvals():
global _defvals
if _defvals is None:
_defvals = {k:Values(Property(k, str(val)).propertyValue) for k, val in iteritems(DEFAULTS)}
return _defvals
def resolve_property(style_map, elem, name):
''' Given a `style_map` previously generated by :func:`resolve_styles()` and
a property `name`, returns the effective value of that property for the
specified element. Handles inheritance and CSS cascading rules. Returns
an instance of :class:`Values`. If the property was never set and
is not a known property, then it will return None. '''
inheritable = name in INHERITED
q = elem
while q is not None:
s = style_map.get(q)
if s is not None:
val = s.get(name)
if val is not None:
return val
q = q.getparent() if inheritable else None
return defvals().get(name)
def resolve_pseudo_property(style_map, pseudo_style_map, elem, prop, name, abort_on_missing=False):
sub_map = pseudo_style_map.get(elem)
if abort_on_missing and sub_map is None:
return None
if sub_map is not None:
prop_map = sub_map.get(prop)
if abort_on_missing and prop_map is None:
return None
if prop_map is not None:
val = prop_map.get(name)
if val is not None:
return val
if name in INHERITED:
return resolve_property(style_map, elem, name)
return defvals().get(name)