Use the new font parsing code for normalize_css

This commit is contained in:
Kovid Goyal 2016-06-30 12:25:28 +05:30
parent b0b7a59afd
commit bbaf9875cc
2 changed files with 33 additions and 48 deletions

View File

@ -14,6 +14,7 @@ try:
except ImportError: except ImportError:
raise RuntimeError('You need cssutils >= 0.9.9 for calibre') raise RuntimeError('You need cssutils >= 0.9.9 for calibre')
from cssutils import profile as cssprofiles, CSSParser from cssutils import profile as cssprofiles, CSSParser
from tinycss.fonts3 import parse_font, serialize_font_family
DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{ DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{
'background-color': 'transparent', 'background-image': 'none', 'background-color': 'transparent', 'background-image': 'none',
@ -118,52 +119,19 @@ def normalize_font(cssvalue, font_family_as_list=False):
composition = font_composition composition = font_composition
val = cssvalue.cssText val = cssvalue.cssText
if val == 'inherit': if val == 'inherit':
return {k:'inherit' for k in composition} ans = {k:'inherit' for k in composition}
if val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}: elif val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}:
return {k:DEFAULTS[k] for k in composition} ans = {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: else:
return {} # unrecognized value ans = {k:DEFAULTS[k] for k in composition}
ans.update(parse_font(val))
return style 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): def normalize_border(name, cssvalue):
style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue) style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue)
@ -300,15 +268,16 @@ def test_normalization(return_tests=False): # {{{
return ans return ans
for raw, expected in { 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'}, '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'}, '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'}, '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 large serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'large'},
'bold italic small-caps larger/normal serif': 'bold italic small-caps larger/normal serif':
{'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger', {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger',
'line-height':'normal', 'font-variant':'small-caps'}, 'line-height':'normal', 'font-variant':'small-caps'},
'2em A B': {'font-family': '"A B"', 'font-size': '2em'},
}.iteritems(): }.iteritems():
val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue
style = normalizers['font']('font', val) style = normalizers['font']('font', val)

View File

@ -7,6 +7,8 @@ __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import re
from future_builtins import map
from tinycss.css21 import CSS21Parser, ParseError from tinycss.css21 import CSS21Parser, ParseError
from .tokenizer import tokenize_grouped from .tokenizer import tokenize_grouped
@ -38,6 +40,18 @@ def parse_font_family_tokens(tokens):
def parse_font_family(css_string): def parse_font_family(css_string):
return parse_font_family_tokens(tokenize_grouped(type('')(css_string).strip())) 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()) GLOBAL_IDENTS = frozenset('inherit initial unset normal'.split())
STYLE_IDENTS = frozenset('italic oblique'.split()) STYLE_IDENTS = frozenset('italic oblique'.split())
VARIANT_IDENTS = frozenset(('small-caps',)) 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()) 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())) 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()) 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): def parse_font(css_string):
# See https://www.w3.org/TR/css-fonts-3/#font-prop # See https://www.w3.org/TR/css-fonts-3/#font-prop