%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/oeb/polish/tests/ |
| Current File : //usr/lib/calibre/calibre/ebooks/oeb/polish/tests/cascade.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2016, Kovid Goyal <kovid at kovidgoyal.net>'
from functools import partial
from css_parser import parseStyle
from calibre.constants import iswindows
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS
from calibre.ebooks.oeb.polish.cascade import iterrules, resolve_styles, DEFAULTS
from calibre.ebooks.oeb.polish.css import remove_property_value
from calibre.ebooks.oeb.polish.embed import find_matching_font
from calibre.ebooks.oeb.polish.container import ContainerBase, href_to_name
from calibre.ebooks.oeb.polish.stats import StatsCollector, font_keys, normalize_font_properties, prepare_font_rule
from calibre.ebooks.oeb.polish.tests.base import BaseTest
from calibre.utils.logging import Log, Stream
from polyglot.builtins import iteritems
class VirtualContainer(ContainerBase):
tweak_mode = True
def __init__(self, files):
s = Stream()
self.log_stream = s.stream
log = Log()
log.outputs = [s]
self.opf_version_parsed = (2, 0, 0)
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]
@property
def spine_names(self):
for name in sorted(self.mime_map):
if self.mime_map[name] in OEB_DOCS:
yield name, True
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())
def test_resolve_styles(self):
def test_property(select, resolve_property, selector, name, val=None):
elem = next(select(selector))
ans = resolve_property(elem, name)
if val is None:
val = str(DEFAULTS[name])
self.assertEqual(val, ans.cssText)
def test_pseudo_property(select, resolve_pseudo_property, selector, prop, name, val=None, abort_on_missing=False):
elem = next(select(selector))
ans = resolve_pseudo_property(elem, prop, name, abort_on_missing=abort_on_missing)
if abort_on_missing:
if val is None:
self.assertTrue(ans is None)
return
if val is None:
val = str(DEFAULTS[name])
self.assertEqual(val, ans.cssText)
def get_maps(html, styles=None, pseudo=False):
html = f'<html><head><link href="styles.css"></head><body>{html}</body></html>'
c = VirtualContainer({'index.html':html, 'styles.css':styles or 'body { color: red; font-family: "Kovid Goyal", sans-serif }'})
resolve_property, resolve_pseudo_property, select = resolve_styles(c, 'index.html')
if pseudo:
tp = partial(test_pseudo_property, select, resolve_pseudo_property)
else:
tp = partial(test_property, select, resolve_property)
return tp
t = get_maps('<p style="margin:11pt"><b>x</b>xx</p>')
t('body', 'color', 'red')
t('p', 'color', 'red')
t('b', 'font-weight', 'bold')
t('p', 'margin-top', '11pt')
t('b', 'margin-top')
t('body', 'display', 'block')
t('b', 'display', 'inline')
t('body', 'font-family', ('"Kovid Goyal"', 'sans-serif'))
for e in ('body', 'p', 'b'):
for prop in 'background-color text-indent'.split():
t(e, prop)
t = get_maps('<p>xxx</p><style>p {color: blue}</style>', 'p {color: red}')
t('p', 'color', 'blue')
t = get_maps('<p style="color: blue">xxx</p>', 'p {color: red}')
t('p', 'color', 'blue')
t = get_maps('<p style="color: blue">xxx</p>', 'p {color: red !important}')
t('p', 'color', 'red')
t = get_maps('<p id="p">xxx</p>', '#p { color: blue } p {color: red}')
t('p', 'color', 'blue')
t = get_maps('<p>xxx</p>', 'p {color: red; color: blue}')
t('p', 'color', 'blue')
t = get_maps('<p>xxx</p><style>p {color: blue}</style>', 'p {color: red; margin:11pt}')
t('p', 'margin-top', '11pt')
t = get_maps('<p></p>', 'p:before { content: "xxx" }', True)
t('p', 'before', 'content', '"xxx"')
t = get_maps('<p></p>', 'body p:before { content: "xxx" } p:before { content: "yyy" }', True)
t('p', 'before', 'content', '"xxx"')
t = get_maps('<p></p>', "p:before { content: 'xxx' } p:first-letter { font-weight: bold }", True)
t('p', 'before', 'content', '"xxx"')
t('p', 'first-letter', 'font-weight', 'bold')
t = get_maps('<p></p>', 'p { font-weight: bold; margin: 11pt } p:before { content: xxx }', True)
t('p', 'before', 'content', 'xxx')
t('p', 'before', 'margin-top', '0')
t('p', 'before', 'font-weight', 'bold')
t('p', 'first-letter', 'content')
t('p', 'first-letter', 'content', abort_on_missing=True)
def test_font_stats(self):
embeds = '@font-face { font-family: X; src: url(X.otf) }\n@font-face { font-family: X; src: url(XB.otf); font-weight: bold }'
def get_stats(html, *fonts):
styles = []
html = f'<html><head><link href="styles.css"></head><body>{html}</body></html>'
files = {'index.html':html, 'X.otf':b'xxx', 'XB.otf': b'xbxb'}
for font in fonts:
styles.append('@font-face {')
for k, v in iteritems(font):
if k == 'src':
files[v] = b'xxx'
v = 'url(%s)' % v
styles.append(f'{k} : {v};')
styles.append('}\n')
html = f'<html><head><link href="styles.css"></head><body>{html}</body></html>'
files['styles.css'] = embeds + '\n'.join(styles)
c = VirtualContainer(files)
return StatsCollector(c, do_embed=True)
def font(family, weight=None, style=None):
f = {}
if weight is not None:
f['font-weight'] = weight
if style is not None:
f['font-style'] = style
f = normalize_font_properties(f)
f['font-family'] = [family]
return f
def font_rule(src, *args, **kw):
ans = font(*args, **kw)
ans['font-family'] = list(map(icu_lower, ans['font-family']))
prepare_font_rule(ans)
ans['src'] = src
return ans
def fkey(*args, **kw):
f = font(*args, **kw)
f['font-family'] = icu_lower(f['font-family'][0])
return frozenset((k, v) for k, v in iteritems(f) if k in font_keys)
def fu(text, *args, **kw):
key = fkey(*args, **kw)
val = font(*args, **kw)
val['text'] = set(text)
val['font-family'] = val['font-family'][0]
return key, val
s = get_stats('<p style="font-family: X">abc<b>d\nef</b><i>ghi</i></p><p style="font-family: U">u</p>')
# The normal font must include ghi as it will be used to simulate
# italic by most rendering engines when the italic font is missing
self.assertEqual(s.font_stats, {'XB.otf':set('def'), 'X.otf':set('abcghi')})
self.assertEqual(s.font_spec_map, {'index.html':set('XU')})
self.assertEqual(s.all_font_rules, {'X.otf':font_rule('X.otf', 'X'), 'XB.otf':font_rule('XB.otf', 'X', 'bold')})
self.assertEqual(set(s.font_rule_map), {'index.html'})
self.assertEqual(s.font_rule_map['index.html'], [font_rule('X.otf', 'X'), font_rule('XB.otf', 'X', 'bold')])
self.assertEqual(set(s.font_usage_map), {'index.html'})
self.assertEqual(s.font_usage_map['index.html'], dict([fu('abc', 'X'), fu('def', 'X', weight='bold'), fu('ghi', 'X', style='italic'), fu('u', 'U')]))
s = get_stats('<p style="font-family: X; text-transform:uppercase">abc</p><b style="font-family: X; font-variant: small-caps">d\nef</b>')
self.assertEqual(s.font_stats, {'XB.otf':set('defDEF'), 'X.otf':set('ABC')})
def test_remove_property_value(self):
style = parseStyle('background-image: url(b.png); background: black url(a.png) fixed')
for prop in style.getProperties(all=True):
remove_property_value(prop, lambda val:'png' in val.cssText)
self.assertEqual('background: black fixed', style.cssText)
def test_fallback_font_matching(self):
def cf(id, weight='normal', style='normal', stretch='normal'):
return {'id':id, 'font-weight':weight, 'font-style':style, 'font-stretch':stretch}
fonts = [cf(1, '500', 'oblique', 'condensed'), cf(2, '300', 'italic', 'normal')]
self.assertEqual(find_matching_font(fonts)['id'], 2)
fonts = [cf(1, '500', 'oblique', 'normal'), cf(2, '300', 'italic', 'normal')]
self.assertEqual(find_matching_font(fonts)['id'], 1)
fonts = [cf(1, '500', 'oblique', 'normal'), cf(2, '200', 'oblique', 'normal')]
self.assertEqual(find_matching_font(fonts)['id'], 1)
fonts = [cf(1, '600', 'oblique', 'normal'), cf(2, '100', 'oblique', 'normal')]
self.assertEqual(find_matching_font(fonts)['id'], 2)
fonts = [cf(1, '600', 'oblique', 'normal'), cf(2, '100', 'oblique', 'normal')]
self.assertEqual(find_matching_font(fonts, '500')['id'], 2)
fonts = [cf(1, '600', 'oblique', 'normal'), cf(2, '100', 'oblique', 'normal')]
self.assertEqual(find_matching_font(fonts, '600')['id'], 1)