From 49f7a4acece50d35cafaac1e36739c95a8f1863a Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 29 Aug 2016 17:50:07 +0200 Subject: [PATCH] Support presentational hints --- weasyprint/css/__init__.py | 377 ++++++++++++++++++++++++++++++++++++- 1 file changed, 373 insertions(+), 4 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 76e3def0..2ff35953 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -202,6 +202,13 @@ def find_stylesheets(element_tree, device_media_type, url_fetcher): href, exc) +def check_style_attribute(parser, element, style_attribute): + declarations, errors = parser.parse_style_attr(style_attribute) + for error in errors: + LOGGER.warning(error) + return element, declarations, element_base_url(element) + + def find_style_attributes(element_tree): """ Yield ``element, declaration, base_url`` for elements with @@ -211,10 +218,363 @@ def find_style_attributes(element_tree): for element in element_tree.iter(): style_attribute = element.get('style') if style_attribute: - declarations, errors = parser.parse_style_attr(style_attribute) - for error in errors: - LOGGER.warning(error) - yield element, declarations, element_base_url(element) + yield check_style_attribute(parser, element, style_attribute) + + +def find_presentational_hints(element_tree): + """ + Yield ``element, declaration, base_url`` for elements with + presentational hints. + """ + parser = PARSER + for element in element_tree.iter(): + if element.tag == 'body': + for prop, position in ( + ('height', 'top'), ('height', 'bottom'), + ('width', 'left'), ('width', 'right')): + style_attribute = None + for prop in ('margin%s' % prop, '%smargin' % prop): + if element.get(prop): + style_attribute = 'margin-%s:%spx' % ( + position, element.get('margin%r' % prop)) + break + if style_attribute: + yield check_style_attribute( + parser, element, style_attribute) + if element.get('background'): + style_attribute = 'background-image:%s' % element.get('background') + yield check_style_attribute(parser, element, style_attribute) + if element.get('bgcolor'): + style_attribute = 'background-color:%s' % element.get('bgcolor') + yield check_style_attribute(parser, element, style_attribute) + if element.get('text'): + style_attribute = 'color:%s' % element.get('text') + yield check_style_attribute(parser, element, style_attribute) + elif element.tag == 'pre': + if 'wrap' in element: + yield check_style_attribute( + parser, element, 'white-space:pre-wrap') + elif element.tag == 'center': + yield check_style_attribute(parser, element, 'text-align:center') + elif element.tag == 'div': + align = element.get('align', '').lower() + if align == 'middle': + yield check_style_attribute( + parser, element, 'text-align:center') + elif align in ('center', 'left', 'right', 'justify'): + yield check_style_attribute( + parser, element, 'text-align:%s' % align) + elif element.tag == 'br': + clear = element.get('clear', '').lower() + if clear: + if clear == 'left': + yield check_style_attribute(parser, element, 'clear:left') + elif clear == 'right': + yield check_style_attribute(parser, element, 'clear:right') + elif clear in ('all', 'both'): + yield check_style_attribute(parser, element, 'clear:both') + elif element.tag == 'font': + if element.get('color'): + yield check_style_attribute( + parser, element, 'color:%s' % element.get('color')) + if element.get('face'): + yield check_style_attribute( + parser, element, 'font-family:%s' % element.get('face')) + if element.get('size'): + size = element.get('size').strip() + relative_plus = size.startswith('+') + relative_minus = size.startswith('-') + if relative_plus or relative_minus: + size = size[1:].strip() + try: + size = int(size) + except ValueError: + LOGGER.warning('Invalid value for size: %s' % size) + else: + font_sizes = { + 1: 'x-small', + 2: 'small', + 3: 'medium', + 4: 'large', + 5: 'x-large', + 6: 'xx-large', + 7: '48px', + } + if relative_plus: + size += 3 + elif relative_minus: + size -= 3 + size = min(1, max(7, size)) + yield check_style_attribute( + parser, element, 'font-size:%s' % font_sizes[size]) + elif element.tag in ('ol', 'ul', 'li'): + if element.get('type'): + if element.tag in ('ol', 'li'): + if element.get('type') == '1': + yield check_style_attribute( + parser, element, 'list-style-type:decimal') + elif element.get('type') == 'a': + yield check_style_attribute( + parser, element, 'list-style-type:lower-alpha') + elif element.get('type') == 'A': + yield check_style_attribute( + parser, element, 'list-style-type:upper-alpha') + elif element.get('type') == 'i': + yield check_style_attribute( + parser, element, 'list-style-type:lower-roman') + elif element.get('type') == 'I': + yield check_style_attribute( + parser, element, 'list-style-type:upper-roman') + elif element.tag in ('ul', 'li'): + if element.get('type').lower() in ('disc', 'circle', 'square'): + yield check_style_attribute( + parser, element, + 'list-style-type:%s' % element.get('type').lower()) + elif element.tag == 'table': + # TODO: Rules for children are not handled + align = element.get('align', '').lower() + if align == 'left': + yield check_style_attribute(parser, element, 'float:left') + elif align == 'right': + yield check_style_attribute(parser, element, 'float:right') + elif align == 'center': + yield check_style_attribute( + parser, element, 'margin-left:auto;margin-right:auto') + rules = element.get('rules', '').lower() + if rules in ('none', 'groups', 'rows', 'cols', 'all'): + yield check_style_attribute( + parser, element, + 'border-style:hidden;border-collapse:collapse') + if 'border' in element: + yield check_style_attribute( + parser, element, 'border-style:outset') + frame = element.get('frame', '').lower() + if frame == 'void': + yield check_style_attribute( + parser, element, 'border-style:hidden') + elif frame == 'above': + yield check_style_attribute( + parser, element, + 'border-style:outset hidden hidden hidden') + elif frame == 'below': + yield check_style_attribute( + parser, element, + 'border-style:hidden hidden outset hidden') + elif frame == 'hsides': + yield check_style_attribute( + parser, element, + 'border-style:outset hidden outset hidden') + elif frame == 'lhs': + yield check_style_attribute( + parser, element, + 'border-style:hidden hidden hidden outset') + elif frame == 'rhs': + yield check_style_attribute( + parser, element, + 'border-style:hidden outset hidden hidden') + elif frame == 'vslides': + yield check_style_attribute( + parser, element, + 'border-style:hidden outset') + elif frame in ('box', 'border'): + yield check_style_attribute( + parser, element, 'border-style:outset') + if element.get('cellspacing'): + yield check_style_attribute( + parser, element, + 'border-spacing:%spx' % element.get('cellspacing')) + if element.get('hspace'): + hspace = element.get('hspace') + if hspace.isdigit(): + hspace += 'px' + yield check_style_attribute( + parser, element, + 'margin-left:%s;margin-right:%s' % (hspace, hspace)) + if element.get('vspace'): + vspace = element.get('vspace') + if vspace.isdigit(): + vspace += 'px' + yield check_style_attribute( + parser, element, + 'margin-top:%s;margin-bottom:%s' % (vspace, vspace)) + if element.get('width'): + style_attribute = 'width:%s' % element.get('width') + if element.get('width').isdigit(): + style_attribute += 'px' + yield check_style_attribute(parser, element, style_attribute) + if element.get('height'): + style_attribute = 'height:%s' % element.get('height') + if element.get('height').isdigit(): + style_attribute += 'px' + yield check_style_attribute(parser, element, style_attribute) + if element.get('background'): + style_attribute = 'background-image:%s' % ( + element.get('background')) + yield check_style_attribute(parser, element, style_attribute) + if element.get('bgcolor'): + style_attribute = 'background-color:%s' % ( + element.get('bgcolor')) + yield check_style_attribute(parser, element, style_attribute) + if element.get('bordercolor'): + style_attribute = 'border-color:%s' % ( + element.get('bordercolor')) + yield check_style_attribute(parser, element, style_attribute) + if element.get('border'): + style_attribute = 'border-width:%spx' % ( + element.get('border')) + yield check_style_attribute(parser, element, style_attribute) + elif element.tag in ('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'): + align = element.get('align', '').lower() + if align == 'absmiddle': + yield check_style_attribute( + parser, element, 'text-align:center') + if align in ('left', 'right', 'justify'): + yield check_style_attribute( + parser, element, 'text-align:%s' % align) + valign = element.get('valign', '').lower() + if valign in ('top', 'middle', 'bottom', 'baseline'): + yield check_style_attribute( + parser, element, 'vertical-align:%s' % valign) + if element.tag in ('td', 'th'): + if 'nowrap' in element: + yield check_style_attribute( + parser, element, 'white-space:nowrap') + if element.get('cellpadding'): + yield check_style_attribute( + parser, element, + 'padding:%spx' % element.get('cellpadding')) + if element.get('width'): + style_attribute = 'width:%s' % element.get('width') + if element.get('width').isdigit(): + style_attribute += 'px' + yield check_style_attribute( + parser, element, style_attribute) + if element.tag in ('tr', 'td', 'th'): + if element.get('height'): + style_attribute = 'height:%s' % element.get('height') + if element.get('height').isdigit(): + style_attribute += 'px' + yield check_style_attribute( + parser, element, style_attribute) + if element.get('background'): + style_attribute = 'background-image:%s' % ( + element.get('background')) + yield check_style_attribute(parser, element, style_attribute) + if element.get('bgcolor'): + style_attribute = 'background-color:%s' % ( + element.get('bgcolor')) + yield check_style_attribute(parser, element, style_attribute) + elif element.tag == 'caption': + align = element.get('align', '').lower() + if align == 'bottom': + yield check_style_attribute( + parser, element, 'caption-side:bottom') + if align in ('left', 'right', 'justify'): + yield check_style_attribute( + parser, element, 'text-align:%s' % align) + elif element.tag == 'col': + if element.get('width'): + style_attribute = 'width:%s' % element.get('width') + if element.get('width').isdigit(): + style_attribute += 'px' + yield check_style_attribute(parser, element, style_attribute) + elif element.tag in ('p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'): + align = element.get('align', '').lower() + if align in ('center', 'left', 'right', 'justify'): + yield check_style_attribute( + parser, element, 'text-align:%s' % align) + elif element.tag == 'hr': + align = element.get('align', '').lower() + if align == 'left': + yield check_style_attribute( + parser, element, 'margin-left:0;margin-right:auto') + elif align == 'right': + yield check_style_attribute( + parser, element, 'margin-left:auto;margin-right:0') + elif align == 'center': + yield check_style_attribute( + parser, element, 'margin-left:auto;margin-right:auto') + size = 0 + if element.get('size'): + try: + size = int(element.get('size')) + except ValueError: + LOGGER.warning('Invalid value for size: %s' % size) + if 'color' in element or 'noshade' in element: + yield check_style_attribute( + parser, element, 'border-style:solid') + if size >= 1: + yield check_style_attribute( + parser, element, 'border-width:%spx' % size / 2) + elif size == 1: + yield check_style_attribute( + parser, element, 'border-bottom-width:0') + elif size > 1: + yield check_style_attribute( + parser, element, 'height:%spx' % (size - 2)) + if element.get('width'): + style_attribute = 'width:%s' % element.get('width') + if element.get('width').isdigit(): + style_attribute += 'px' + yield check_style_attribute(parser, element, style_attribute) + if element.get('color'): + yield check_style_attribute( + parser, element, 'color:%s' % element.get('color')) + elif element.tag in ( + 'iframe', 'applet', 'embed', 'img', 'input', 'object'): + if (element.tag != 'input' or + element.get('type', '').lower() == 'image'): + align = element.get('align', '').lower() + if align in ('left', 'right'): + yield check_style_attribute( + parser, element, 'float:%s' % align) + elif align in ('top', 'baseline', 'bottom'): + yield check_style_attribute( + parser, element, 'vertical-align:%s' % align) + elif align == 'texttop': + yield check_style_attribute( + parser, element, 'vertical-align:text-top') + elif align in ('absmiddle', 'abscenter', 'middle', 'center'): + # TODO: middle and center values are wrong + yield check_style_attribute( + parser, element, 'vertical-align:middle') + if element.get('hspace'): + hspace = element.get('hspace') + if hspace.isdigit(): + hspace += 'px' + yield check_style_attribute( + parser, element, + 'margin-left:%s;margin-right:%s' % (hspace, hspace)) + if element.get('vspace'): + vspace = element.get('vspace') + if vspace.isdigit(): + vspace += 'px' + yield check_style_attribute( + parser, element, + 'margin-top:%s;margin-bottom:%s' % (vspace, vspace)) + # TODO: img seems to be excluded for width and height, but a + # lot of W3C tests rely on this attribute being applied to img + if element.get('width'): + style_attribute = 'width:%s' % element.get('width') + if element.get('width').isdigit(): + style_attribute += 'px' + yield check_style_attribute( + parser, element, style_attribute) + if element.get('height'): + style_attribute = 'height:%s' % element.get('height') + if element.get('height').isdigit(): + style_attribute += 'px' + yield check_style_attribute( + parser, element, style_attribute) + if element.tag in ('img', 'object', 'input'): + if element.get('border'): + yield check_style_attribute( + parser, element, + 'border-width:%spx;border-style:solid' % + element.get('border')) + if element.tag == 'iframe': + if element.get('frameborder', '').lower() in ('0', 'no'): + yield check_style_attribute(parser, element, 'border:none') def evaluate_media_query(query_list, device_media_type): @@ -485,6 +845,15 @@ def get_all_computed_styles(html, user_stylesheets=None): cascaded_styles, name, values, weight, element, pseudo_type) + specificity = (0, 0, 0, 0) + for element, declarations, base_url in find_presentational_hints( + element_tree): + for name, values, importance in preprocess_declarations( + base_url, declarations): + precedence = declaration_precedence('author', importance) + weight = (precedence, specificity) + add_declaration(cascaded_styles, name, values, weight, element) + specificity = (1, 0, 0, 0) for element, declarations, base_url in find_style_attributes(element_tree): for name, values, importance in preprocess_declarations(