diff --git a/src/calibre/ebooks/oeb/polish/cascade.py b/src/calibre/ebooks/oeb/polish/cascade.py index 677c693a67..c5efea8853 100644 --- a/src/calibre/ebooks/oeb/polish/cascade.py +++ b/src/calibre/ebooks/oeb/polish/cascade.py @@ -29,40 +29,55 @@ def html_css_stylesheet(container): 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, rules, sheet_name, media_rule_ok=media_allowed, rule_index_counter=None, rule_type=None): +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 + :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 (by default all rules are yielded + :param rule_type: Only yield rules of this type, where type is a string type name, see cssutils.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() - riter = partial(iterrules, container, rule_index_counter=rule_index_counter, media_rule_ok=media_rule_ok, rule_type=rule_type) + 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: - name = container.href_to_name(rule.href, sheet_name) - if container.has_name(name): - csheet = container.parsed(name) - if isinstance(csheet, CSSStyleSheet): - for cr in riter(csheet, name): - yield cr + 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('Recursive import of {} from {}, ignoring'.format(name, sheet_name)) + else: + csheet = container.parsed(name) + if isinstance(csheet, CSSStyleSheet): + for cr in riter(name, rules=csheet): + yield cr elif rule.type == CSSRule.MEDIA_RULE: if media_rule_ok(rule.media): - for cr in riter(rule.cssRules, sheet_name): + for cr in riter(sheet_name, rules=rule.cssRules): yield cr 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') @@ -123,7 +138,7 @@ def resolve_styles(container, name): pseudo_pat = re.compile(ur':{1,2}(%s)' % ('|'.join(INAPPROPRIATE_PSEUDO_CLASSES)), re.I) def process_sheet(sheet, sheet_name): - for rule, sheet_name, rule_index in iterrules(container, sheet, sheet_name, rule_index_counter=rule_index_counter, rule_type=CSSRule.STYLE_RULE): + for rule, sheet_name, rule_index in iterrules(container, sheet_name, rules=sheet, rule_index_counter=rule_index_counter, rule_type=CSSRule.STYLE_RULE): for selector in rule.selectorList: text = selector.selectorText try: diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 83ad3fc8dd..a84d5c6968 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -186,7 +186,6 @@ class ContainerBase(object): # {{{ css_preprocessor=(None if self.tweak_mode else self.css_preprocessor)) # }}} - class Container(ContainerBase): # {{{ ''' diff --git a/src/calibre/ebooks/oeb/polish/tests/cascade.py b/src/calibre/ebooks/oeb/polish/tests/cascade.py new file mode 100644 index 0000000000..06b0e1e75f --- /dev/null +++ b/src/calibre/ebooks/oeb/polish/tests/cascade.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python2 +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +from calibre.constants import iswindows + +from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS +from calibre.ebooks.oeb.polish.cascade import iterrules +from calibre.ebooks.oeb.polish.container import ContainerBase, href_to_name +from calibre.ebooks.oeb.polish.tests.base import BaseTest +from calibre.utils.logging import Log, Stream + +class VirtualContainer(ContainerBase): + + tweak_mode = True + + def __init__(self, files): + s = Stream() + self.log_stream = s.stream + log = Log() + log.outputs = [s] + ContainerBase.__init__(self, log=log) + self.mime_map = {k:self.guess_type(k) for k in files} + self.files = files + + def has_name(self, name): + return name in self.mime_map + + def href_to_name(self, href, base=None): + return href_to_name(href, ('C:\\root' if iswindows else '/root'), base) + + def parsed(self, name): + if name not in self.parsed_cache: + mt = self.mime_map[name] + if mt in OEB_STYLES: + self.parsed_cache[name] = self.parse_css(self.files[name], name) + elif mt in OEB_DOCS: + self.parsed_cache[name] = self.parse_xhtml(self.files[name], name) + else: + self.parsed_cache[name] = self.files[name] + return self.parsed_cache[name] + +class CascadeTest(BaseTest): + + def test_iterrules(self): + def get_rules(files, name='x/one.css', l=1, rule_type=None): + c = VirtualContainer(files) + rules = tuple(iterrules(c, name, rule_type=rule_type)) + self.assertEqual(len(rules), l) + return rules, c + get_rules({'x/one.css':'@import "../two.css";', 'two.css':'body { color: red; }'}) + get_rules({'x/one.css':'@import "../two.css" screen;', 'two.css':'body { color: red; }'}) + get_rules({'x/one.css':'@import "../two.css" xyz;', 'two.css':'body { color: red; }'}, l=0) + get_rules({'x/one.css':'@import "../two.css";', 'two.css':'body { color: red; }'}, l=0, rule_type='FONT_FACE_RULE') + get_rules({'x/one.css':'@import "../two.css";', 'two.css':'body { color: red; }'}, rule_type='STYLE_RULE') + get_rules({'x/one.css':'@media screen { body { color: red; } }'}) + get_rules({'x/one.css':'@media xyz { body { color: red; } }'}, l=0) + c = get_rules({'x/one.css':'@import "../two.css";', 'two.css':'@import "x/one.css"; body { color: red; }'})[1] + self.assertIn('Recursive import', c.log_stream.getvalue().decode('utf-8')) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 593d9b5ea2..b97bd9ec60 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -50,7 +50,6 @@ FONT_SIZE_NAMES = { 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' } -ALL_MEDIA_TYPES = frozenset('all aural braille handheld print projection screen tty tv embossed amzn-mobi amzn-kf8'.split()) ALLOWED_MEDIA_TYPES = frozenset({'screen', 'all', 'aural', 'amzn-kf8'}) IGNORED_MEDIA_FEATURES = frozenset('width min-width max-width height min-height max-height device-width min-device-width max-device-width device-height min-device-height max-device-height aspect-ratio min-aspect-ratio max-aspect-ratio device-aspect-ratio min-device-aspect-ratio max-device-aspect-ratio color min-color max-color color-index min-color-index max-color-index monochrome min-monochrome max-monochrome -webkit-min-device-pixel-ratio resolution min-resolution max-resolution scan grid'.split()) # noqa diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index 045ff251f5..7b88c9cd11 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -27,6 +27,10 @@ class Stream(object): def flush(self): self.stream.flush() + def prints(self, level, *args, **kwargs): + self._prints(*args, **kwargs) + + class ANSIStream(Stream): def __init__(self, stream=sys.stdout):