def test_parentRule(self): "CSSStyleDeclaration.parentRule" s = css_parser.css.CSSStyleDeclaration() sheet = css_parser.css.CSSStyleRule() s.parentRule = sheet self.assertEqual(sheet, s.parentRule) sheet = css_parser.parseString('a{x:1}') s = sheet.cssRules[0] d = s.style self.assertEqual(s, d.parentRule) s = css_parser.parseString(''' @font-face { font-weight: bold; } a { font-weight: bolder; } @page { font-weight: bolder; } ''') for r in s: self.assertEqual(r.style.parentRule, r)
def test_handlers(self): "css_parser.log" s = self._setHandler() css_parser.log.setLevel(logging.FATAL) self.assertEqual(css_parser.log.getEffectiveLevel(), logging.FATAL) css_parser.parseString('a { color: 1 }') self.assertEqual(s.getvalue(), '') css_parser.log.setLevel(logging.DEBUG) css_parser.parseString('a { color: 1 }') self.assertEqual( s.getvalue(), 'ERROR Property: Invalid value for "CSS Level 2.1" property: 1 [1:5: color]\n' ) s = self._setHandler() css_parser.log.setLevel(logging.WARNING) css_parser.parseUrl('http://example.com') q = s.getvalue()[:38] if q.startswith('WARNING URLError'): pass else: self.assertEqual(q, 'ERROR Expected "text/css" mime type')
def test_comments(self): "PropertyValue with comment" # issue #45 for t in ( 'green', 'green /* comment */', '/* comment */green', '/* comment */green/* comment */', '/* comment */ green /* comment */', '/* comment *//**/ green /* comment *//**/', ): sheet = css_parser.parseString('body {color: %s; }' % t) p = sheet.cssRules[0].style.getProperties()[0] self.assertEqual(p.valid, True) for t in ( 'gree', 'gree /* comment */', '/* comment */gree', '/* comment */gree/* comment */', '/* comment */ gree /* comment */', '/* comment *//**/ gree /* comment *//**/', ): sheet = css_parser.parseString('body {color: %s; }' % t) p = sheet.cssRules[0].style.getProperties()[0] self.assertEqual(p.valid, False)
def test_cssRules(self): "CSSMediaRule.cssRules" r = css_parser.css.CSSMediaRule() self.assertEqual([], r.cssRules) sr = css_parser.css.CSSStyleRule() r.cssRules.append(sr) self.assertEqual([sr], r.cssRules) ir = css_parser.css.CSSImportRule() self.assertRaises(xml.dom.HierarchyRequestErr, r.cssRules.append, ir) s = css_parser.parseString('@media all { /*1*/a {x:1} }') m = s.cssRules[0] self.assertEqual(2, m.cssRules.length) del m.cssRules[0] self.assertEqual(1, m.cssRules.length) m.cssRules.append('/*2*/') self.assertEqual(2, m.cssRules.length) m.cssRules.extend(css_parser.parseString('/*3*/x {y:2}').cssRules) self.assertEqual(4, m.cssRules.length) self.assertEqual( '@media all {\n a {\n x: 1\n }\n /*2*/\n /*3*/\n x {\n y: 2\n }\n }', m.cssText) for rule in m.cssRules: self.assertEqual(rule.parentStyleSheet, s) self.assertEqual(rule.parentRule, m)
def test_parsevalidation(self): style = 'color: 1' t = 'a { %s }' % style css_parser.log.setLevel(logging.DEBUG) # sheet s = self._setHandler() css_parser.parseString(t) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseString(t, validate=False) self.assertEqual(s.getvalue(), '') # style s = self._setHandler() css_parser.parseStyle(style) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseStyle(style, validate=True) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseStyle(style, validate=False) self.assertEqual(s.getvalue(), '')
def test_set(self): "settings.set()" css_parser.ser.prefs.useMinified() text = 'a {filter: progid:DXImageTransform.Microsoft.BasicImage( rotation = 90 )}' self.assertEqual(css_parser.parseString(text).cssText, ''.encode()) css_parser.settings.set('DXImageTransform.Microsoft', True) self.assertEqual(css_parser.parseString(text).cssText, 'a{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=90)}'.encode()) css_parser.ser.prefs.useDefaults()
def test_attributes(self): "css_parser.parseString(href, media)" s = css_parser.parseString("a{}", href="file:foo.css", media="screen, projection, tv") self.assertEqual(s.href, "file:foo.css") self.assertEqual(s.media.mediaText, "screen, projection, tv") s = css_parser.parseString("a{}", href="file:foo.css", media=["screen", "projection", "tv"]) self.assertEqual(s.media.mediaText, "screen, projection, tv")
def test_escapes(self): "css_parser escapes" css = r'\43\x { \43\x: \43\x !import\41nt }' sheet = css_parser.parseString(css) self.assertEqual(sheet.cssText, r'''C\x { c\x: C\x !important }'''.encode()) css = r'\ x{\ x :\ x ;y:1} ' sheet = css_parser.parseString(css) self.assertEqual(sheet.cssText, r'''\ x { \ x: \ x; y: 1 }'''.encode())
def test_omitLastSemicolon(self): "Preferences.omitLastSemicolon" css = 'a { x: 1; y: 2 }' s = css_parser.parseString(css) self.assertEqual('a {\n x: 1;\n y: 2\n }'.encode(), s.cssText) css_parser.ser.prefs.omitLastSemicolon = False self.assertEqual('a {\n x: 1;\n y: 2;\n }'.encode(), s.cssText)
def test_defaultPropertyPriority(self): "Preferences.defaultPropertyPriority" css = 'a {\n color: green !IM\\portant\n }' s = css_parser.parseString(css) self.assertEqual(s.cssText, 'a {\n color: green !important\n }'.encode()) css_parser.ser.prefs.defaultPropertyPriority = False self.assertEqual(s.cssText, css.encode())
def test_styleSheet(self): "CSSImportRule.styleSheet" def fetcher(url): if url == "/root/level1/anything.css": return None, '@import "level2/css.css" "title2";' else: return None, 'a { color: red }' parser = css_parser.CSSParser(fetcher=fetcher) sheet = parser.parseString('''@charset "ascii"; @import "level1/anything.css" tv "title";''', href='/root/') self.assertEqual(sheet.href, '/root/') ir = sheet.cssRules[1] self.assertEqual(ir.href, 'level1/anything.css') self.assertEqual(ir.styleSheet.href, '/root/level1/anything.css') # inherits ascii as no self charset is set self.assertEqual(ir.styleSheet.encoding, 'ascii') self.assertEqual(ir.styleSheet.ownerRule, ir) self.assertEqual(ir.styleSheet.media.mediaText, 'tv') self.assertEqual(ir.styleSheet.parentStyleSheet, None) # sheet self.assertEqual(ir.styleSheet.title, 'title') self.assertEqual(ir.styleSheet.cssText, '@charset "ascii";\n@import "level2/css.css" "title2";'.encode()) ir2 = ir.styleSheet.cssRules[1] self.assertEqual(ir2.href, 'level2/css.css') self.assertEqual(ir2.styleSheet.href, '/root/level1/level2/css.css') # inherits ascii as no self charset is set self.assertEqual(ir2.styleSheet.encoding, 'ascii') self.assertEqual(ir2.styleSheet.ownerRule, ir2) self.assertEqual(ir2.styleSheet.media.mediaText, 'all') self.assertEqual(ir2.styleSheet.parentStyleSheet, None) # ir.styleSheet self.assertEqual(ir2.styleSheet.title, 'title2') self.assertEqual(ir2.styleSheet.cssText, '@charset "ascii";\na {\n color: red\n }'.encode()) sheet = css_parser.parseString('@import "CANNOT-FIND.css";') ir = sheet.cssRules[0] self.assertEqual(ir.href, "CANNOT-FIND.css") self.assertEqual(type(ir.styleSheet), css_parser.css.CSSStyleSheet) def fetcher(url): if url.endswith('level1.css'): return None, '@charset "ascii"; @import "level2.css";'.encode() else: return None, 'a { color: red }'.encode() parser = css_parser.CSSParser(fetcher=fetcher) sheet = parser.parseString('@charset "iso-8859-1";@import "level1.css";') self.assertEqual(sheet.encoding, 'iso-8859-1') sheet = sheet.cssRules[1].styleSheet self.assertEqual(sheet.encoding, 'ascii') sheet = sheet.cssRules[1].styleSheet self.assertEqual(sheet.encoding, 'ascii')
def test_replaceUrls(self): "css_parser.replaceUrls()" css_parser.ser.prefs.keepAllProperties = True css = r''' @import "im1"; @import url(im2); a { background-image: url(c) !important; background-\image: url(b); background: url(a) no-repeat !important; }''' s = css_parser.parseString(css) css_parser.replaceUrls(s, lambda old: "NEW" + old) self.assertEqual('@import "NEWim1";', s.cssRules[0].cssText) self.assertEqual('NEWim2', s.cssRules[1].href) self.assertEqual( '''background-image: url(NEWc) !important; background-\\image: url(NEWb); background: url(NEWa) no-repeat !important''', s.cssRules[2].style.cssText) css_parser.ser.prefs.keepAllProperties = False # CSSStyleDeclaration style = css_parser.parseStyle('''color: red; background-image: url(1.png), url('2.png')''') css_parser.replaceUrls(style, lambda url: 'prefix/' + url) self.assertEqual( style.cssText, '''color: red; background-image: url(prefix/1.png), url(prefix/2.png)''')
def test_list(self): "PropertyValue[index]" # issue #41 css = """div.one {color: rgb(255, 0, 0);} """ sheet = css_parser.parseString(css) pv = sheet.cssRules[0].style.getProperty('color').propertyValue self.assertEqual(pv.value, 'rgb(255, 0, 0)') self.assertEqual(pv[0].value, 'rgb(255, 0, 0)') # issue #42 sheet = css_parser.parseString('body { font-family: "A", b, serif }') pv = sheet.cssRules[0].style.getProperty('font-family').propertyValue self.assertEqual(3, pv.length) self.assertEqual(pv[0].value, 'A') self.assertEqual(pv[1].value, 'b') self.assertEqual(pv[2].value, 'serif')
def test_getUrls(self): "css_parser.getUrls()" css_parser.ser.prefs.keepAllProperties = True css = r''' @import "im1"; @import url(im2); @import url( im3 ); @import url( "im4" ); @import url( 'im5' ); a { background-image: url(a) !important; background-\image: url(b); background: url(c) no-repeat !important; /* issue #46 */ src: local("xx"), url("f.woff") format("woff"), url("f.otf") format("opentype"), url("f.svg#f") format("svg"); }''' urls = set(css_parser.getUrls(css_parser.parseString(css))) self.assertEqual( urls, set([ "im1", "im2", "im3", "im4", "im5", "a", "b", "c", 'f.woff', 'f.svg#f', 'f.otf' ])) css_parser.ser.prefs.keepAllProperties = False
def test_keepComments(self): "Preferences.keepComments" s = css_parser.parseString('/*1*/ a { /*2*/ }') css_parser.ser.prefs.keepComments = False self.assertEqual(''.encode(), s.cssText) css_parser.ser.prefs.keepEmptyRules = True self.assertEqual('a {}'.encode(), s.cssText)
def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: with open(P('templates/html.css'), 'rb') as f: html_css = f.read().decode('utf-8') _html_css_stylesheet = parseString(html_css, validate=False) return _html_css_stylesheet
def test_keepUsedNamespaceRulesOnly(self): "Preferences.keepUsedNamespaceRulesOnly" tests = { # default == prefix => both are combined '@namespace p "u"; @namespace "u"; p|a, a {top: 0}': ('@namespace "u";\na, a {\n top: 0\n }', '@namespace "u";\na, a {\n top: 0\n }'), '@namespace "u"; @namespace p "u"; p|a, a {top: 0}': ('@namespace p "u";\np|a, p|a {\n top: 0\n }', '@namespace p "u";\np|a, p|a {\n top: 0\n }'), # default and prefix '@namespace p "u"; @namespace "d"; p|a, a {top: 0}': ('@namespace p "u";\n@namespace "d";\np|a, a {\n top: 0\n }', '@namespace p "u";\n@namespace "d";\np|a, a {\n top: 0\n }' ), # prefix only '@namespace p "u"; @namespace "d"; p|a {top: 0}': ('@namespace p "u";\n@namespace "d";\np|a {\n top: 0\n }', '@namespace p "u";\np|a {\n top: 0\n }'), # default only '@namespace p "u"; @namespace "d"; a {top: 0}': ('@namespace p "u";\n@namespace "d";\na {\n top: 0\n }', '@namespace "d";\na {\n top: 0\n }'), # prefix-ns only '@namespace p "u"; @namespace d "d"; p|a {top: 0}': ('@namespace p "u";\n@namespace d "d";\np|a {\n top: 0\n }', '@namespace p "u";\np|a {\n top: 0\n }'), } for test in tests: s = css_parser.parseString(test) expwith, expwithout = tests[test] css_parser.ser.prefs.keepUsedNamespaceRulesOnly = False self.assertEqual(s.cssText, expwith.encode()) css_parser.ser.prefs.keepUsedNamespaceRulesOnly = True self.assertEqual(s.cssText, expwithout.encode())
def test_minimizeColorHash(self): "Preferences.minimizeColorHash" css = 'a { color: #ffffff }' s = css_parser.parseString(css) self.assertEqual('a {\n color: #fff\n }'.encode(), s.cssText) css_parser.ser.prefs.minimizeColorHash = False self.assertEqual('a {\n color: #ffffff\n }'.encode(), s.cssText)
def test_propertyNameSpacer(self): "Preferences.propertyNameSpacer" css = 'a { x: 1; y: 2 }' s = css_parser.parseString(css) self.assertEqual('a {\n x: 1;\n y: 2\n }'.encode(), s.cssText) css_parser.ser.prefs.propertyNameSpacer = '' self.assertEqual('a {\n x:1;\n y:2\n }'.encode(), s.cssText)
def test_linesAfterRule(self): "Preferences.linesAfterRule" s = css_parser.parseString( 'div {color:red;} @media screen {.aclass {width: 200px}}') expected_default = '''\ div { color: red } @media screen { .aclass { width: 200px } }''' self.assertEqual(expected_default.encode(), s.cssText) css_parser.ser.prefs.linesAfterRules = 1 * '\n' expected_changed = '''\ div { color: red } @media screen { .aclass { width: 200px } } ''' self.assertEqual(expected_changed.encode(), s.cssText)
def test_keepUnknownAtRules(self): "Preferences.keepUnknownAtRules" tests = { '''@three-dee { @background-lighting { azimuth: 30deg; elevation: 190deg; } h1 { color: red } } h1 { color: blue }''': ('''@three-dee { @background-lighting { azimuth: 30deg; elevation: 190deg; } h1 { color: red } } h1 { color: blue }''', '''h1 { color: blue }''') } for test in tests: s = css_parser.parseString(test) expwith, expwithout = tests[test] css_parser.ser.prefs.keepUnknownAtRules = True self.assertEqual(s.cssText, expwith.encode()) css_parser.ser.prefs.keepUnknownAtRules = False self.assertEqual(s.cssText, expwithout.encode())
def test_incomplete(self): "PropertyValue (incomplete)" tests = {'url("a': 'url(a)', 'url(a': 'url(a)'} for v, exp in tests.items(): s = css_parser.parseString('a { background: %s' % v) v = s.cssRules[0].style.background self.assertEqual(v, exp)
def do_filter_css(self, css): from css_parser import parseString from css_parser.css import CSSRule sheet = parseString(css, validate=False) rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) sel_map = {} count = 0 for r in rules: # Check if we have only class selectors for this rule nc = [ x for x in r.selectorList if not x.selectorText.startswith('.') ] if len(r.selectorList) > 1 and not nc: # Replace all the class selectors with a single class selector # This will be added to the class attribute of all elements # that have one of these selectors. replace_name = 'c_odt%d' % count count += 1 for sel in r.selectorList: s = sel.selectorText[1:] if s not in sel_map: sel_map[s] = [] sel_map[s].append(replace_name) r.selectorText = '.' + replace_name return sheet.cssText, sel_map
def _get_css_links( content: bytes, base_url: str, headers: _Headers, ) -> Iterable[str]: """Get all links from a CSS file.""" parsed = css_parser.parseString(content) return list(css_parser.getUrls(parsed))
def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: with open(pkg_resources.resource_filename('ebook_converter', 'data/html.css'), 'rb') as f: html_css = f.read().decode('utf-8') _html_css_stylesheet = parseString(html_css, validate=False) return _html_css_stylesheet
def test_defaultAtKeyword(self): "Preferences.defaultAtKeyword" s = css_parser.parseString('@im\\port "x";') self.assertEqual('@import "x";'.encode(), s.cssText) css_parser.ser.prefs.defaultAtKeyword = True self.assertEqual('@import "x";'.encode(), s.cssText) css_parser.ser.prefs.defaultAtKeyword = False self.assertEqual('@im\\port "x";'.encode(), s.cssText)
def get_css_links( css_file: BinaryIO, base_url: str, headers: Optional[List[Tuple[str, str]]] = None, ) -> Iterable[str]: """Get all links from a CSS file.""" text = css_file.read() parsed = css_parser.parseString(text) yield from css_parser.getUrls(parsed)
def test_CSSStyleSheet(self): "CSSSerializer.do_CSSStyleSheet" css = '/* κουρος */' sheet = css_parser.parseString(css) ans = sheet.cssText if isinstance(ans, bytes): ans = ans.decode('utf-8') self.assertEqual(css, ans) css = '@charset "utf-8";\n/* κουρος */' sheet = css_parser.parseString(css) ans = sheet.cssText if isinstance(ans, bytes): ans = ans.decode('utf-8') self.assertEqual(css, ans) sheet.cssRules[0].encoding = 'ascii' self.assertEqual('@charset "ascii";\n/* \\3BA \\3BF \\3C5 \\3C1 \\3BF \\3C2 */'.encode(), sheet.cssText)
def test_lineSeparator(self): "Preferences.lineSeparator" s = css_parser.parseString('a { x:1;y:2}') self.assertEqual('a {\n x: 1;\n y: 2\n }'.encode(), s.cssText) # cannot be indented as no split possible css_parser.ser.prefs.lineSeparator = '' self.assertEqual('a {x: 1;y: 2 }'.encode(), s.cssText) # no valid css but should work css_parser.ser.prefs.lineSeparator = 'XXX' self.assertEqual('a {XXX x: 1;XXX y: 2XXX }'.encode(), s.cssText)
def test_parentRule(self): "CSSVariablesDeclaration.parentRule" s = css_parser.parseString('@variables { a:1}') r = s.cssRules[0] d = r.variables self.assertEqual(r, d.parentRule) d2 = css_parser.css.CSSVariablesDeclaration('b: 2') r.variables = d2 self.assertEqual(r, d2.parentRule)
def __call__(self, oeb): if not self.body_font_family: return None if not self.href: iid, href = oeb.manifest.generate(u'page_styles', u'page_styles.css') rules = [css_text(x) for x in self.rules] rules = u'\n\n'.join(rules) sheet = css_parser.parseString(rules, validate=False) self.href = oeb.manifest.add(iid, href, guess_type(href)[0], data=sheet).href return self.href
def replace_css(self, css): manifest = self.oeb.manifest for item in manifest.values(): if item.media_type in OEB_STYLES: manifest.remove(item) id, href = manifest.generate('css', 'stylesheet.css') sheet = css_parser.parseString(css, validate=False) if self.transform_css_rules: from calibre.ebooks.css_transform_rules import transform_sheet transform_sheet(self.transform_css_rules, sheet) item = manifest.add(id, href, CSS_MIME, data=sheet) self.oeb.manifest.main_stylesheet = item return href
def get_embed_font_info(self, family, failure_critical=True): efi = [] body_font_family = None if not family: return body_font_family, efi from calibre.utils.fonts.scanner import font_scanner, NoFonts from calibre.utils.fonts.utils import panose_to_css_generic_family try: faces = font_scanner.fonts_for_family(family) except NoFonts: msg = (u'No embeddable fonts found for family: %r'%family) if failure_critical: raise ValueError(msg) self.oeb.log.warn(msg) return body_font_family, efi if not faces: msg = (u'No embeddable fonts found for family: %r'%family) if failure_critical: raise ValueError(msg) self.oeb.log.warn(msg) return body_font_family, efi for i, font in enumerate(faces): ext = 'otf' if font['is_otf'] else 'ttf' fid, href = self.oeb.manifest.generate(id=u'font', href=u'fonts/%s.%s'%(ascii_filename(font['full_name']).replace(u' ', u'-'), ext)) item = self.oeb.manifest.add(fid, href, guess_type('dummy.'+ext)[0], data=font_scanner.get_font_data(font)) item.unload_data_from_memory() cfont = { u'font-family':u'"%s"'%font['font-family'], u'panose-1': u' '.join(map(unicode_type, font['panose'])), u'src': u'url(%s)'%item.href, } if i == 0: generic_family = panose_to_css_generic_family(font['panose']) body_font_family = u"'%s',%s"%(font['font-family'], generic_family) self.oeb.log(u'Embedding font: %s'%font['font-family']) for k in (u'font-weight', u'font-style', u'font-stretch'): if font[k] != u'normal': cfont[k] = font[k] rule = '@font-face { %s }'%('; '.join(u'%s:%s'%(k, v) for k, v in iteritems(cfont))) rule = css_parser.parseString(rule) efi.append(rule) return body_font_family, efi
def dup_data(self): ''' Duplicate data so that any changes we make to markup/CSS only affect KF8 output and not MOBI 6 output ''' self._data_cache = {} # Suppress css_parser logging output as it is duplicated anyway earlier # in the pipeline css_parser.log.setLevel(logging.CRITICAL) for item in self.oeb.manifest: if item.media_type in XML_DOCS: self._data_cache[item.href] = copy.deepcopy(item.data) elif item.media_type in OEB_STYLES: # I can't figure out how to make an efficient copy of the # in-memory CSSStylesheet, as deepcopy doesn't work (raises an # exception) self._data_cache[item.href] = css_parser.parseString( item.data.cssText, validate=False)
def replace_resource_links(self): ''' Replace links to resources (raster images/fonts) with pointers to the MOBI record containing the resource. The pointers are of the form: kindle:embed:XXXX?mime=image/* The ?mime= is apparently optional and not used for fonts. ''' def pointer(item, oref): ref = urlnormalize(item.abshref(oref)) idx = self.resources.item_map.get(ref, None) if idx is not None: is_image = self.resources.records[idx-1][:4] not in {b'FONT'} idx = to_ref(idx) if is_image: self.used_images.add(ref) return 'kindle:embed:%s?mime=%s'%(idx, self.resources.mime_map[ref]) else: return 'kindle:embed:%s'%idx return oref for item in self.oeb.manifest: if item.media_type in XML_DOCS: root = self.data(item) for tag in XPath('//h:img|//svg:image')(root): for attr, ref in iteritems(tag.attrib): if attr.split('}')[-1].lower() in {'src', 'href'}: tag.attrib[attr] = pointer(item, ref) for tag in XPath('//h:style')(root): if tag.text: sheet = css_parser.parseString(tag.text, validate=False) replacer = partial(pointer, item) css_parser.replaceUrls(sheet, replacer, ignoreImportRules=True) repl = sheet.cssText if isbytestring(repl): repl = repl.decode('utf-8') tag.text = '\n'+ repl + '\n' elif item.media_type in OEB_STYLES: sheet = self.data(item) replacer = partial(pointer, item) css_parser.replaceUrls(sheet, replacer, ignoreImportRules=True)
def collect_global_css(self): global_css = defaultdict(list) for item in self.items: stylizer = self.stylizers[item] if float(self.context.margin_top) >= 0: stylizer.page_rule['margin-top'] = '%gpt'%\ float(self.context.margin_top) if float(self.context.margin_bottom) >= 0: stylizer.page_rule['margin-bottom'] = '%gpt'%\ float(self.context.margin_bottom) items = sorted(stylizer.page_rule.items()) css = ';\n'.join("%s: %s" % (key, val) for key, val in items) css = ('@page {\n%s\n}\n'%css) if items else '' rules = [css_text(r) for r in stylizer.font_face_rules + self.embed_font_rules] raw = '\n\n'.join(rules) css += '\n\n' + raw global_css[css].append(item) gc_map = {} manifest = self.oeb.manifest for css in global_css: href = None if css.strip(): id_, href = manifest.generate('page_css', 'page_styles.css') sheet = css_parser.parseString(css, validate=False) if self.transform_css_rules: from calibre.ebooks.css_transform_rules import transform_sheet transform_sheet(self.transform_css_rules, sheet) manifest.add(id_, href, CSS_MIME, data=sheet) gc_map[css] = href ans = {} for css, items in iteritems(global_css): for item in items: ans[item] = gc_map[css] return ans
def do_filter_css(self, css): from css_parser import parseString from css_parser.css import CSSRule sheet = parseString(css, validate=False) rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) sel_map = {} count = 0 for r in rules: # Check if we have only class selectors for this rule nc = [x for x in r.selectorList if not x.selectorText.startswith('.')] if len(r.selectorList) > 1 and not nc: # Replace all the class selectors with a single class selector # This will be added to the class attribute of all elements # that have one of these selectors. replace_name = 'c_odt%d'%count count += 1 for sel in r.selectorList: s = sel.selectorText[1:] if s not in sel_map: sel_map[s] = [] sel_map[s].append(replace_name) r.selectorText = '.'+replace_name return sheet.cssText, sel_map
def __init__(self, tree, path, oeb, opts, profile=None, extra_css='', user_css='', base_css=''): self.oeb, self.opts = oeb, opts self.profile = profile if self.profile is None: # Use the default profile. This should really be using # opts.output_profile, but I don't want to risk changing it, as # doing so might well have hard to debug font size effects. from calibre.customize.ui import output_profiles for x in output_profiles(): if x.short_name == 'default': self.profile = x break if self.profile is None: # Just in case the default profile is removed in the future :) self.profile = opts.output_profile self.body_font_size = self.profile.fbase self.logger = oeb.logger item = oeb.manifest.hrefs[path] basename = os.path.basename(path) cssname = os.path.splitext(basename)[0] + '.css' stylesheets = [html_css_stylesheet()] if base_css: stylesheets.append(parseString(base_css, validate=False)) style_tags = xpath(tree, '//*[local-name()="style" or local-name()="link"]') # Add css_parser parsing profiles from output_profile for profile in self.opts.output_profile.extra_css_modules: cssprofiles.addProfile(profile['name'], profile['props'], profile['macros']) parser = CSSParser(fetcher=self._fetch_css_file, log=logging.getLogger('calibre.css')) self.font_face_rules = [] for elem in style_tags: if (elem.tag == XHTML('style') and elem.get('type', CSS_MIME) in OEB_STYLES and media_ok(elem.get('media'))): text = elem.text if elem.text else u'' for x in elem: t = getattr(x, 'text', None) if t: text += u'\n\n' + force_unicode(t, u'utf-8') t = getattr(x, 'tail', None) if t: text += u'\n\n' + force_unicode(t, u'utf-8') if text: text = oeb.css_preprocessor(text) # We handle @import rules separately parser.setFetcher(lambda x: ('utf-8', b'')) stylesheet = parser.parseString(text, href=cssname, validate=False) parser.setFetcher(self._fetch_css_file) for rule in stylesheet.cssRules: if rule.type == rule.IMPORT_RULE: ihref = item.abshref(rule.href) if not media_ok(rule.media.mediaText): continue hrefs = self.oeb.manifest.hrefs if ihref not in hrefs: self.logger.warn('Ignoring missing stylesheet in @import rule:', rule.href) continue sitem = hrefs[ihref] if sitem.media_type not in OEB_STYLES: self.logger.warn('CSS @import of non-CSS file %r' % rule.href) continue stylesheets.append(sitem.data) # Make links to resources absolute, since these rules will # be folded into a stylesheet at the root replaceUrls(stylesheet, item.abshref, ignoreImportRules=True) stylesheets.append(stylesheet) elif (elem.tag == XHTML('link') and elem.get('href') and elem.get( 'rel', 'stylesheet').lower() == 'stylesheet' and elem.get( 'type', CSS_MIME).lower() in OEB_STYLES and media_ok(elem.get('media')) ): href = urlnormalize(elem.attrib['href']) path = item.abshref(href) sitem = oeb.manifest.hrefs.get(path, None) if sitem is None: self.logger.warn( 'Stylesheet %r referenced by file %r not in manifest' % (path, item.href)) continue if not hasattr(sitem.data, 'cssRules'): self.logger.warn( 'Stylesheet %r referenced by file %r is not CSS'%(path, item.href)) continue stylesheets.append(sitem.data) csses = {'extra_css':extra_css, 'user_css':user_css} for w, x in csses.items(): if x: try: text = x stylesheet = parser.parseString(text, href=cssname, validate=False) stylesheets.append(stylesheet) except: self.logger.exception('Failed to parse %s, ignoring.'%w) self.logger.debug('Bad css: ') self.logger.debug(x) rules = [] index = 0 self.stylesheets = set() self.page_rule = {} for sheet_index, stylesheet in enumerate(stylesheets): href = stylesheet.href self.stylesheets.add(href) for rule in stylesheet.cssRules: if rule.type == rule.MEDIA_RULE: if media_ok(rule.media.mediaText): for subrule in rule.cssRules: rules.extend(self.flatten_rule(subrule, href, index, is_user_agent_sheet=sheet_index==0)) index += 1 else: rules.extend(self.flatten_rule(rule, href, index, is_user_agent_sheet=sheet_index==0)) index = index + 1 rules.sort(key=itemgetter(0)) # sort by specificity self.rules = rules self._styles = {} pseudo_pat = re.compile(u':{1,2}(%s)' % ('|'.join(INAPPROPRIATE_PSEUDO_CLASSES)), re.I) select = Select(tree, ignore_inappropriate_pseudo_classes=True) for _, _, cssdict, text, _ in rules: fl = pseudo_pat.search(text) try: matches = tuple(select(text)) except SelectorError as err: self.logger.error('Ignoring CSS rule with invalid selector: %r (%s)' % (text, as_unicode(err))) continue if fl is not None: fl = fl.group(1) if fl == 'first-letter' and getattr(self.oeb, 'plumber_output_format', '').lower() in {u'mobi', u'docx'}: # Fake first-letter for elem in matches: for x in elem.iter('*'): if x.text: punctuation_chars = [] text = unicode_type(x.text) while text: category = unicodedata.category(text[0]) if category[0] not in {'P', 'Z'}: break punctuation_chars.append(text[0]) text = text[1:] special_text = u''.join(punctuation_chars) + \ (text[0] if text else u'') span = x.makeelement('{%s}span' % XHTML_NS) span.text = special_text span.set('data-fake-first-letter', '1') span.tail = text[1:] x.text = None x.insert(0, span) self.style(span)._update_cssdict(cssdict) break else: # Element pseudo-class for elem in matches: self.style(elem)._update_pseudo_class(fl, cssdict) else: for elem in matches: self.style(elem)._update_cssdict(cssdict) for elem in xpath(tree, '//h:*[@style]'): self.style(elem)._apply_style_attr(url_replacer=item.abshref) num_pat = re.compile(r'[0-9.]+$') for elem in xpath(tree, '//h:img[@width or @height]'): style = self.style(elem) # Check if either height or width is not default is_styled = style._style.get('width', 'auto') != 'auto' or \ style._style.get('height', 'auto') != 'auto' if not is_styled: # Update img style dimension using width and height upd = {} for prop in ('width', 'height'): val = elem.get(prop, '').strip() try: del elem.attrib[prop] except: pass if val: if num_pat.match(val) is not None: val += 'px' upd[prop] = val if upd: style._update_cssdict(upd)
def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: html_css = open(P('templates/html.css'), 'rb').read() _html_css_stylesheet = parseString(html_css, validate=False) return _html_css_stylesheet
def extract_css_into_flows(self): inlines = defaultdict(list) # Ensure identical <style>s not repeated sheets = {} passthrough = getattr(self.opts, 'mobi_passthrough', False) for item in self.oeb.manifest: if item.media_type in OEB_STYLES: sheet = self.data(item) if not passthrough and not self.opts.expand_css and hasattr(item.data, 'cssText'): condense_sheet(sheet) sheets[item.href] = len(self.flows) self.flows.append(sheet) def fix_import_rules(sheet): changed = False for rule in sheet.cssRules.rulesOfType(CSSRule.IMPORT_RULE): if rule.href: href = item.abshref(rule.href) idx = sheets.get(href, None) if idx is not None: idx = to_ref(idx) rule.href = 'kindle:flow:%s?mime=text/css'%idx changed = True return changed for item in self.oeb.spine: root = self.data(item) for link in XPath('//h:link[@href]')(root): href = item.abshref(link.get('href')) idx = sheets.get(href, None) if idx is not None: idx = to_ref(idx) link.set('href', 'kindle:flow:%s?mime=text/css'%idx) for tag in XPath('//h:style')(root): p = tag.getparent() idx = p.index(tag) raw = tag.text if not raw or not raw.strip(): extract(tag) continue sheet = css_parser.parseString(raw, validate=False) if fix_import_rules(sheet): raw = force_unicode(sheet.cssText, 'utf-8') repl = etree.Element(XHTML('link'), type='text/css', rel='stylesheet') repl.tail='\n' p.insert(idx, repl) extract(tag) inlines[raw].append(repl) for raw, elems in iteritems(inlines): idx = to_ref(len(self.flows)) self.flows.append(raw) for link in elems: link.set('href', 'kindle:flow:%s?mime=text/css'%idx) for item in self.oeb.manifest: if item.media_type in OEB_STYLES: sheet = self.data(item) if hasattr(sheet, 'cssRules'): fix_import_rules(sheet) for i, sheet in enumerate(tuple(self.flows)): if hasattr(sheet, 'cssText'): self.flows[i] = force_unicode(sheet.cssText, 'utf-8')