diff --git a/src/tinycss/fonts3.py b/src/tinycss/fonts3.py index f443c50d8f..4795f11642 100644 --- a/src/tinycss/fonts3.py +++ b/src/tinycss/fonts3.py @@ -11,7 +11,7 @@ from tinycss.css21 import CSS21Parser, ParseError from .tokenizer import tokenize_grouped -def parse_font_family(css_string): +def parse_font_family_tokens(tokens): families = [] current_family = '' def commit(): @@ -19,7 +19,7 @@ def parse_font_family(css_string): if val: families.append(val) - for token in tokenize_grouped(css_string.strip()): + for token in tokens: if token.type == 'STRING': if current_family: commit() @@ -35,6 +35,113 @@ def parse_font_family(css_string): commit() return families +def parse_font_family(css_string): + return parse_font_family_tokens(tokenize_grouped(type('')(css_string).strip())) + +GLOBAL_IDENTS = frozenset('inherit initial unset normal'.split()) +STYLE_IDENTS = frozenset('italic oblique'.split()) +VARIANT_IDENTS = frozenset(('small-caps',)) +WEIGHT_IDENTS = frozenset('bold bolder lighter'.split()) +STRETCH_IDENTS = frozenset('ultra-condensed extra-condensed condensed semi-condensed semi-expanded expanded extra-expanded ultra-expanded'.split()) +BEFORE_SIZE_IDENTS = STYLE_IDENTS | VARIANT_IDENTS | WEIGHT_IDENTS | STRETCH_IDENTS +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()) + +def parse_font(css_string): + # See https://www.w3.org/TR/css-fonts-3/#font-prop + style = variant = weight = stretch = size = height = None + tokens = list(reversed(tuple(tokenize_grouped(type('')(css_string).strip())))) + if tokens and tokens[-1].value in LEGACY_FONT_SPEC: + return {'font-family':['sans-serif']} + while tokens: + tok = tokens.pop() + if tok.type == 'STRING': + tokens.append(tok) + break + if tok.type == 'INTEGER': + if size is None: + if weight is None and tok.value in WEIGHT_SIZES: + weight = tok.as_css() + continue + break + if height is None: + height = tok.as_css() + break + break + if tok.type == 'NUMBER': + if size is not None and height is None: + height = tok.as_css() + break + if tok.type == 'DELIM': + if tok.value == '/' and size is not None and height is None: + continue + break + if tok.type in ('DIMENSION', 'PERCENTAGE'): + if size is None: + size = tok.as_css() + continue + if height is None: + height = tok.as_css() + break + if tok.type == 'IDENT': + if tok.value in GLOBAL_IDENTS: + if size is not None: + if height is None: + height = tok.value + else: + tokens.append(tok) + break + if style is None: + style = tok.value + elif variant is None: + variant = tok.value + elif weight is None: + weight = tok.value + elif stretch is None: + stretch = tok.value + elif size is None: + size = tok.value + elif height is None: + height = tok.value + break + else: + tokens.append(tok) + break + continue + if tok.value in BEFORE_SIZE_IDENTS: + if size is not None: + break + if tok.value in STYLE_IDENTS: + style = tok.value + elif tok.value in VARIANT_IDENTS: + variant = tok.value + elif tok.value in WEIGHT_IDENTS: + weight = tok.value + elif tok.value in STRETCH_IDENTS: + stretch = tok.value + elif tok.value in SIZE_IDENTS: + size = tok.value + else: + tokens.append(tok) + break + families = parse_font_family_tokens(reversed(tokens)) + ans = {} + if style is not None: + ans['font-style'] = style + if variant is not None: + ans['font-variant'] = variant + if weight is not None: + ans['font-weight'] = weight + if stretch is not None: + ans['font-stretch'] = stretch + if size is not None: + ans['font-size'] = size + if height is not None: + ans['line-height'] = height + if families: + ans['font-family'] = families + return ans class FontFaceRule(object): diff --git a/src/tinycss/tests/fonts3.py b/src/tinycss/tests/fonts3.py index 0fdb6c5699..d5a29e5114 100644 --- a/src/tinycss/tests/fonts3.py +++ b/src/tinycss/tests/fonts3.py @@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' -from tinycss.fonts3 import CSSFonts3Parser, parse_font_family +from tinycss.fonts3 import CSSFonts3Parser, parse_font_family, parse_font from tinycss.tests import BaseTest class TestFonts3(BaseTest): @@ -48,4 +48,16 @@ class TestFonts3(BaseTest): for single in ('serif', 'sans-serif', 'A B C'): self.ae([single], parse_font_family(single)) - + def test_parse_font(self): + def t(raw, **kw): + q = {('line' if k == 'height' else 'font') + '-' + k:v for k, v in kw.iteritems()} + self.ae(q, parse_font(raw)) + t('caption', family=['sans-serif']) + t('serif', family=['serif']) + t('12pt/14pt sans-serif', size='12pt', height='14pt', family=['sans-serif']) + t('80% sans-serif', size='80%', family=['sans-serif']) + t('x-large/110% "new century schoolbook", serif', size='x-large', height='110%', family=['new century schoolbook', 'serif']) + t('bold italic large Palatino, serif', weight='bold', style='italic', size='large', family=['Palatino', 'serif']) + t('normal small-caps 120%/120% fantasy', style='normal', variant='small-caps', size='120%', height='120%', family=['fantasy']) + t('condensed oblique 12pt Helvetica Neue, serif', stretch='condensed', style='oblique', size='12pt', family=['Helvetica Neue', 'serif']) + t('300 italic 1.3em/1.7em FB Armada, sans-serif', weight='300', style='italic', size='1.3em', height='1.7em', family=['FB Armada', 'sans-serif'])