diff --git a/src/calibre/ebooks/oeb/normalize_css.py b/src/calibre/ebooks/oeb/normalize_css.py index b00e54ca23..559324dd78 100644 --- a/src/calibre/ebooks/oeb/normalize_css.py +++ b/src/calibre/ebooks/oeb/normalize_css.py @@ -14,6 +14,7 @@ try: except ImportError: raise RuntimeError('You need cssutils >= 0.9.9 for calibre') from cssutils import profile as cssprofiles, CSSParser +from tinycss.fonts3 import parse_font, serialize_font_family DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{ 'background-color': 'transparent', 'background-image': 'none', @@ -118,52 +119,19 @@ def normalize_font(cssvalue, font_family_as_list=False): composition = font_composition val = cssvalue.cssText if val == 'inherit': - return {k:'inherit' for k in composition} - if val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}: - return {k:DEFAULTS[k] for k in composition} - if getattr(cssvalue, 'length', 1) < 2: - return {} # Mandatory to define both font size and font family - style = {k:DEFAULTS[k] for k in composition} - families = [] - vals = [x.cssText for x in cssvalue] - found_font_size = False - while vals: - text = vals.pop() - if not families and text == 'inherit': - families.append(text) - continue - if cssprofiles.validate('line-height', text): - if not vals or not cssprofiles.validate('font-size', vals[-1]): - if cssprofiles.validate('font-size', text): - style['font-size'] = text - found_font_size = True - break - return {} # must have font-size here - style['line-height'] = text - style['font-size'] = vals.pop() - found_font_size = True - break - if cssprofiles.validate('font-size', text): - style['font-size'] = text - found_font_size = True - break - if families == ['inherit']: - return {} # Cannot have multiple font-families if the last one if inherit - families.insert(0, text) - if not families or not found_font_size: - return {} # font-family required - style['font-family'] = families if font_family_as_list else ', '.join(families) - props = ['font-style', 'font-variant', 'font-weight'] - while vals: - for i, prop in enumerate(tuple(props)): - if cssprofiles.validate(prop, vals[0]): - props.pop(i) - style[prop] = vals.pop(0) - break - else: - return {} # unrecognized value - - return style + ans = {k:'inherit' for k in composition} + elif val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}: + ans = {k:DEFAULTS[k] for k in composition} + else: + ans = {k:DEFAULTS[k] for k in composition} + ans.update(parse_font(val)) + if font_family_as_list: + if isinstance(ans['font-family'], basestring): + ans['font-family'] = [x.strip() for x in ans['font-family'].split(',')] + else: + if not isinstance(ans['font-family'], basestring): + ans['font-family'] = serialize_font_family(ans['font-family']) + return ans def normalize_border(name, cssvalue): style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue) @@ -300,15 +268,16 @@ def test_normalization(return_tests=False): # {{{ return ans for raw, expected in { - 'some_font': {}, 'none': {}, 'inherit':{k:'inherit' for k in font_composition}, + 'some_font': {'font-family':'some_font'}, 'inherit':{k:'inherit' for k in font_composition}, '1.2pt/1.4 A_Font': {'font-family':'A_Font', 'font-size':'1.2pt', 'line-height':'1.4'}, - 'bad font': {}, '10% serif': {'font-family':'serif', 'font-size':'10%'}, + 'bad font': {'font-family':'"bad font"'}, '10% serif': {'font-family':'serif', 'font-size':'10%'}, '12px "My Font", serif': {'font-family':'"My Font", serif', 'font-size': '12px'}, 'normal 0.6em/135% arial,sans-serif': {'font-family': 'arial, sans-serif', 'font-size': '0.6em', 'line-height':'135%', 'font-style':'normal'}, 'bold italic large serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'large'}, 'bold italic small-caps larger/normal serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger', 'line-height':'normal', 'font-variant':'small-caps'}, + '2em A B': {'font-family': '"A B"', 'font-size': '2em'}, }.iteritems(): val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue style = normalizers['font']('font', val) diff --git a/src/tinycss/fonts3.py b/src/tinycss/fonts3.py index 4795f11642..cb54066407 100644 --- a/src/tinycss/fonts3.py +++ b/src/tinycss/fonts3.py @@ -7,6 +7,8 @@ __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' +import re +from future_builtins import map from tinycss.css21 import CSS21Parser, ParseError from .tokenizer import tokenize_grouped @@ -38,6 +40,18 @@ def parse_font_family_tokens(tokens): def parse_font_family(css_string): return parse_font_family_tokens(tokenize_grouped(type('')(css_string).strip())) +def serialize_font_family(families): + def one(x): + xl = x.lower() + if xl in GENERIC_FAMILIES: + if xl == 'sansserif': + xl = 'sans-serif' + return xl + if SIMPLE_NAME_PAT.match(x) is not None: + return x + return '"%s"' % x.replace('"', r'\"') + return ', '.join(map(one, families)) + GLOBAL_IDENTS = frozenset('inherit initial unset normal'.split()) STYLE_IDENTS = frozenset('italic oblique'.split()) VARIANT_IDENTS = frozenset(('small-caps',)) @@ -47,6 +61,8 @@ BEFORE_SIZE_IDENTS = STYLE_IDENTS | VARIANT_IDENTS | WEIGHT_IDENTS | STRETCH_IDE SIZE_IDENTS = frozenset('xx-small x-small small medium large x-large xx-large larger smaller'.split()) WEIGHT_SIZES = frozenset(map(int, '100 200 300 400 500 600 700 800 900'.split())) LEGACY_FONT_SPEC = frozenset('caption icon menu message-box small-caption status-bar'.split()) +GENERIC_FAMILIES = frozenset('serif sans-serif sansserif cursive fantasy monospace'.split()) +SIMPLE_NAME_PAT = re.compile(r'[a-zA-Z][a-zA-Z0-9_-]*$') def parse_font(css_string): # See https://www.w3.org/TR/css-fonts-3/#font-prop