%PDF- %PDF-
Direktori : /lib/calibre/tinycss/ |
Current File : //lib/calibre/tinycss/fonts3.py |
#!/usr/bin/env python3 # vim:fileencoding=utf-8 __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' import re from tinycss.css21 import CSS21Parser, ParseError from .tokenizer import tokenize_grouped def parse_font_family_tokens(tokens): families = [] current_family = '' def commit(): val = current_family.strip() if val: families.append(val) for token in tokens: if token.type == 'STRING': if current_family: commit() current_family = token.value elif token.type == 'DELIM': if token.value == ',': if current_family: commit() current_family = '' elif token.type == 'IDENT': current_family += ' ' + token.value if current_family: commit() return families def parse_font_family(css_string): return parse_font_family_tokens(tokenize_grouped(type('')(css_string).strip())) def serialize_single_font_family(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 and not x.lower().startswith('and'): # css_parser dies if a font name starts with and return x return '"%s"' % x.replace('"', r'\"') def serialize_font_family(families): return ', '.join(map(serialize_single_font_family, families)) 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()) 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 serialize_font(font_dict): ans = [] for x in 'style variant weight stretch'.split(): val = font_dict.get('font-' + x) if val is not None: ans.append(val) val = font_dict.get('font-size') if val is not None: fs = val val = font_dict.get('line-height') if val is not None: fs += '/' + val ans.append(fs) val = font_dict.get('font-family') if val: ans.append(serialize_font_family(val)) return ' '.join(ans) 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: at_keyword = '@font-face' __slots__ = 'declarations', 'line', 'column' def __init__(self, declarations, line, column): self.declarations = declarations self.line = line self.column = column def __repr__(self): return ('<{0.__class__.__name__} at {0.line}:{0.column}>' .format(self)) class CSSFonts3Parser(CSS21Parser): ''' Parse @font-face rules from the CSS 3 fonts module ''' ALLOWED_CONTEXTS_FOR_FONT_FACE = {'stylesheet', '@media', '@page'} def __init__(self): super(CSSFonts3Parser, self).__init__() self.at_parsers['@font-face'] = self.parse_font_face_rule def parse_font_face_rule(self, rule, previous_rules, errors, context): if context not in self.ALLOWED_CONTEXTS_FOR_FONT_FACE: raise ParseError(rule, '@font-face rule not allowed in ' + context) if rule.body is None: raise ParseError(rule, 'invalid {0} rule: missing block'.format(rule.at_keyword)) if rule.head: raise ParseError(rule, '{0} rule is not allowed to have content before the descriptor declaration'.format(rule.at_keyword)) declarations, decerrors = self.parse_declaration_list(rule.body) errors.extend(decerrors) return FontFaceRule(declarations, rule.line, rule.column)