diff --git a/weasyprint/__init__.py b/weasyprint/__init__.py index 527ae360a..7f384b2fb 100644 --- a/weasyprint/__init__.py +++ b/weasyprint/__init__.py @@ -332,7 +332,6 @@ def __init__(self, guess=None, filename=None, url=None, file_obj=None, self.base_url = base_url self.matcher = matcher or cssselect2.Matcher() self.page_rules = [] if page_rules is None else page_rules - # TODO: fonts are stored here and should be cleaned after rendering self.fonts = [] preprocess_stylesheet( media_type, base_url, stylesheet, url_fetcher, self.matcher, diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index ff7863fa9..6984bd46c 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -34,7 +34,8 @@ from .validation.descriptors import preprocess_descriptors # Reject anything not in here: -PSEUDO_ELEMENTS = (None, 'before', 'after', 'first-line', 'first-letter') +PSEUDO_ELEMENTS = ( + None, 'before', 'after', 'marker', 'first-line', 'first-letter') PageType = namedtuple('PageType', ['side', 'blank', 'first', 'index', 'name']) @@ -67,11 +68,14 @@ def __init__(self, html, sheets, presentational_hints, target_collector): for specificity, attributes in find_style_attributes( html.etree_element, presentational_hints, html.base_url): element, declarations, base_url = attributes + style = cascaded_styles.setdefault((element, None), {}) 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) + old_weight = style.get(name, (None, None))[1] + if old_weight is None or old_weight <= weight: + style[name] = values, weight # First, add declarations and set computed styles for "real" elements # *in tree order*. Tree order is important so that parents have @@ -84,12 +88,14 @@ def __init__(self, html, sheets, presentational_hints, target_collector): for selector in sheet.matcher.match(element): specificity, order, pseudo_type, declarations = selector specificity = sheet_specificity or specificity + style = cascaded_styles.setdefault( + (element.etree_element, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence(origin, importance) weight = (precedence, specificity) - add_declaration( - cascaded_styles, name, values, weight, - element.etree_element, pseudo_type) + old_weight = style.get(name, (None, None))[1] + if old_weight is None or old_weight <= weight: + style[name] = values, weight parent = element.parent.etree_element if element.parent else None self.set_computed_styles( element.etree_element, root=html.etree_element, parent=parent, @@ -165,13 +171,15 @@ def add_page_declarations(self, page_type): specificity, pseudo_type, selector_page_type = selector if self._page_type_match(selector_page_type, page_type): specificity = sheet_specificity or specificity + style = self._cascaded_styles.setdefault( + (page_type, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence( origin, importance) weight = (precedence, specificity) - add_declaration( - self._cascaded_styles, name, values, weight, - page_type, pseudo_type) + old_weight = style.get(name, (None, None))[1] + if old_weight is None or old_weight <= weight: + style[name] = values, weight def get_cascaded_styles(self): return self._cascaded_styles @@ -573,19 +581,6 @@ def declaration_precedence(origin, importance): return 5 -def add_declaration(cascaded_styles, prop_name, prop_values, weight, element, - pseudo_type=None): - """Set the value for a property on a given element. - - The value is only set if there is no value of greater weight defined yet. - - """ - style = cascaded_styles.setdefault((element, pseudo_type), {}) - _values, previous_weight = style.get(prop_name, (None, None)) - if previous_weight is None or previous_weight <= weight: - style[prop_name] = prop_values, weight - - def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, root_style=None, base_url=None, target_collector=None): @@ -602,8 +597,10 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, # border-*-style is none, so border-width computes to zero. # Other than that, properties that would need computing are # border-*-color, but they do not apply. - for side in ('top', 'bottom', 'left', 'right'): - computed['border_%s_width' % side] = 0 + computed['border_top_width'] = 0 + computed['border_bottom_width'] = 0 + computed['border_left_width'] = 0 + computed['border_right_width'] = 0 computed['outline_width'] = 0 return computed @@ -614,10 +611,10 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, if parent_style: for name in parent_style: if name.startswith('__'): - specified[name] = parent_style[name] + computed[name] = specified[name] = parent_style[name] for name in cascaded: if name.startswith('__'): - specified[name] = cascaded[name][0] + computed[name] = specified[name] = cascaded[name][0] for name, initial in INITIAL_VALUES.items(): if name in cascaded: @@ -761,7 +758,7 @@ def parse_page_selectors(rule): types['index'] = (*nth_values, group) # TODO: specificity is not specified yet - # https://github.com/w3c/csswg-drafts/issues/3791 + # https://github.com/w3c/csswg-drafts/issues/3524 types['specificity'][1] += 1 continue diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 87c83ed44..9a6a9cb5e 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -197,29 +197,20 @@ def compute(element, pseudo_type, specified, computed, parent_style, """ from .validation.properties import PROPERTIES - def computer(): - """Dummy object that holds attributes.""" - return 0 - - computer.is_root_element = parent_style is None - if parent_style is None: - parent_style = INITIAL_VALUES - - computer.element = element - computer.pseudo_type = pseudo_type - computer.specified = specified - computer.computed = computed - computer.parent_style = parent_style - computer.root_style = root_style - computer.base_url = base_url - computer.target_collector = target_collector + computer = { + 'is_root_element': parent_style is None, + 'element': element, + 'pseudo_type': pseudo_type, + 'specified': specified, + 'computed': computed, + 'parent_style': parent_style or INITIAL_VALUES, + 'root_style': root_style, + 'base_url': base_url, + 'target_collector': target_collector, + } getter = COMPUTER_FUNCTIONS.get - for name in specified: - if name.startswith('__'): - computed[name] = specified[name] - for name in COMPUTING_ORDER: if name in computed: # Already computed @@ -354,24 +345,24 @@ def length(computer, name, value, font_size=None, pixels_only=False): result = value.value * LENGTHS_TO_PIXELS[unit] elif unit in ('em', 'ex', 'ch', 'rem'): if font_size is None: - font_size = computer.computed['font_size'] + font_size = computer['computed']['font_size'] if unit == 'ex': # TODO: cache - result = value.value * font_size * ex_ratio(computer.computed) + result = value.value * font_size * ex_ratio(computer['computed']) elif unit == 'ch': # TODO: cache # TODO: use context to use @font-face fonts layout = text.Layout( context=None, font_size=font_size, - style=computer.computed) + style=computer['computed']) layout.set_text('0') line, _ = layout.get_first_line() - logical_width, _ = text.get_size(line, computer.computed) + logical_width, _ = text.get_size(line, computer['computed']) result = value.value * logical_width elif unit == 'em': result = value.value * font_size elif unit == 'rem': - result = value.value * computer.root_style['font_size'] + result = value.value * computer['root_style']['font_size'] else: # A percentage or 'auto': no conversion needed. return value @@ -385,7 +376,7 @@ def length(computer, name, value, font_size=None, pixels_only=False): @register_computer('bleed-bottom') def bleed(computer, name, value): if value == 'auto': - if 'crop' in computer.computed['marks']: + if 'crop' in computer['computed']['marks']: return Dimension(8, 'px') # 6pt else: return Dimension(0, 'px') @@ -418,7 +409,7 @@ def background_size(computer, name, values): @register_computer('outline-width') def border_width(computer, name, value): """Compute the ``border-*-width`` properties.""" - style = computer.computed[name.replace('width', 'style')] + style = computer['computed'][name.replace('width', 'style')] if style in ('none', 'hidden'): return 0 @@ -461,11 +452,11 @@ def compute_attr_function(computer, values): func_name, value = values assert func_name == 'attr()' attr_name, type_or_unit, fallback = value - # computer.element sometimes is None - # computer.element sometimes is a 'PageType' object without .get() + # computer['element'] sometimes is None + # computer['element'] sometimes is a 'PageType' object without .get() # so wrapt the .get() into try and return None instead of crashing try: - attr_value = computer.element.get(attr_name, fallback) + attr_value = computer['element'].get(attr_name, fallback) if type_or_unit == 'string': pass # Keep the string elif type_or_unit == 'url': @@ -473,7 +464,7 @@ def compute_attr_function(computer, values): attr_value = ('internal', unquote(attr_value[1:])) else: attr_value = ( - 'external', safe_urljoin(computer.base_url, attr_value)) + 'external', safe_urljoin(computer['base_url'], attr_value)) elif type_or_unit == 'color': attr_value = parse_color(attr_value.strip()) elif type_or_unit == 'integer': @@ -519,12 +510,12 @@ def _content_list(computer, values): (attr,) + value[1][1:])) else: computed_value = value - if computer.target_collector and computed_value: - computer.target_collector.collect_computed_target( + if computer['target_collector'] and computed_value: + computer['target_collector'].collect_computed_target( computed_value[1][0]) if computed_value is None: LOGGER.warning('Unable to compute %s\'s value for content: %s' % ( - computer.element, ', '.join(str(item) for item in value))) + computer['element'], ', '.join(str(item) for item in value))) else: computed_values.append(computed_value) @@ -552,7 +543,7 @@ def content(computer, name, values): if len(values) == 1: value, = values if value == 'normal': - return 'inhibit' if computer.pseudo_type else 'contents' + return 'inhibit' if computer['pseudo_type'] else 'contents' elif value == 'none': return 'inhibit' return _content_list(computer, values) @@ -565,10 +556,10 @@ def display(computer, name, value): See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo """ - float_ = computer.specified['float'] - position = computer.specified['position'] + float_ = computer['specified']['float'] + position = computer['specified']['position'] if position in ('absolute', 'fixed') or float_ != 'none' or \ - computer.is_root_element: + computer['is_root_element']: if value == 'inline-table': return'table' elif value in ('inline', 'table-row-group', 'table-column', @@ -586,7 +577,7 @@ def compute_float(computer, name, value): See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo """ - if computer.specified['position'] in ('absolute', 'fixed'): + if computer['specified']['position'] in ('absolute', 'fixed'): return 'none' else: return value @@ -599,7 +590,7 @@ def font_size(computer, name, value): return FONT_SIZE_KEYWORDS[value] # TODO: support 'larger' and 'smaller' - parent_font_size = computer.parent_style['font_size'] + parent_font_size = computer['parent_style']['font_size'] if value.unit == '%': return value.value * parent_font_size / 100. else: @@ -615,7 +606,7 @@ def font_weight(computer, name, value): elif value == 'bold': return 700 elif value in ('bolder', 'lighter'): - parent_value = computer.parent_style['font_weight'] + parent_value = computer['parent_style']['font_weight'] return FONT_WEIGHT_RELATIVE[value][parent_value] else: return value @@ -630,7 +621,7 @@ def line_height(computer, name, value): return ('NUMBER', value.value) elif value.unit == '%': factor = value.value / 100. - font_size_value = computer.computed['font_size'] + font_size_value = computer['computed']['font_size'] pixels = factor * font_size_value else: pixels = length(computer, name, value, pixels_only=True) @@ -642,8 +633,8 @@ def anchor(computer, name, values): """Compute the ``anchor`` property.""" if values != 'none': _, key = values - anchor_name = computer.element.get(key) or None - computer.target_collector.collect_anchor(anchor_name) + anchor_name = computer['element'].get(key) or None + computer['target_collector'].collect_anchor(anchor_name) return anchor_name @@ -656,7 +647,7 @@ def link(computer, name, values): type_, value = values if type_ == 'attr()': return get_link_attribute( - computer.element, value, computer.base_url) + computer['element'], value, computer['base_url']) else: return values @@ -669,7 +660,7 @@ def lang(computer, name, values): else: type_, key = values if type_ == 'attr()': - return computer.element.get(key) or None + return computer['element'].get(key) or None elif type_ == 'string': return key @@ -703,11 +694,11 @@ def vertical_align(computer, name, value): 'top', 'bottom'): return value elif value == 'super': - return computer.computed['font_size'] * 0.5 + return computer['computed']['font_size'] * 0.5 elif value == 'sub': - return computer.computed['font_size'] * -0.5 + return computer['computed']['font_size'] * -0.5 elif value.unit == '%': - height, _ = strut_layout(computer.computed) + height, _ = strut_layout(computer['computed']) return height * value.value / 100. else: return length(computer, name, value, pixels_only=True) diff --git a/weasyprint/css/html5_ua.css b/weasyprint/css/html5_ua.css index 80a441b66..0b38a54a1 100644 --- a/weasyprint/css/html5_ua.css +++ b/weasyprint/css/html5_ua.css @@ -473,6 +473,7 @@ track { display: none; } tt { font-family: monospace; } u { text-decoration: underline; } +::marker { unicode-bidi: isolate; font-variant-numeric: tabular-nums; } ul { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; unicode-bidi: isolate; } *[dir=ltr] ul { padding-left: 40px; padding-right: 0; } diff --git a/weasyprint/css/tests_ua.css b/weasyprint/css/tests_ua.css index 91c2ee4f8..3b81c4692 100644 --- a/weasyprint/css/tests_ua.css +++ b/weasyprint/css/tests_ua.css @@ -32,3 +32,5 @@ h3 { bookmark-level: 3; bookmark-label: content(text); } h4 { bookmark-level: 4; bookmark-label: content(text); } h5 { bookmark-level: 5; bookmark-label: content(text); } h6 { bookmark-level: 6; bookmark-label: content(text); } + +::marker { unicode-bidi: isolate; font-variant-numeric: tabular-nums; } diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py index 4f60653f8..cabd56032 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -456,14 +456,13 @@ def expand_font(name, tokens): # Make `tokens` a stack tokens = list(reversed(tokens)) - # Values for font-style font-variant and font-weight can come in any - # order and are all optional. - while tokens: + # Values for font-style, font-variant-caps, font-weight and font-stretch + # can come in any order and are all optional. + for _ in range(4): token = tokens.pop() if get_keyword(token) == 'normal': # Just ignore 'normal' keywords. Unspecified properties will get - # their initial token, which is 'normal' for all three here. - # TODO: fail if there is too many 'normal' values? + # their initial token, which is 'normal' for all four here. continue if font_style([token]) is not None: @@ -475,10 +474,15 @@ def expand_font(name, tokens): elif font_stretch([token]) is not None: suffix = '-stretch' else: - # We’re done with these three, continue with font-size + # We’re done with these four, continue with font-size break yield suffix, [token] + if not tokens: + raise InvalidValues + else: + token = tokens.pop() + # Then font-size is mandatory # Latest `token` from the loop. if font_size([token]) is None: diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index 810939d7a..c606bc7b1 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -31,8 +31,6 @@ # returning a value or None for invalid. # For properties that take a single value, that value is returned by itself # instead of a list. -# TODO: fix this: Also transform values: keyword and URLs are returned as -# strings. PROPERTIES = {} @@ -192,7 +190,10 @@ def list_style_image(token, base_url): @property() def transform_origin(tokens): - # TODO: parse (and ignore) a third value for Z. + """``transform-origin`` property validation.""" + if len(tokens) == 3: + # Ignore third parameter as 3D transforms are ignored. + tokens = tokens[:2] return parse_2d_position(tokens) diff --git a/weasyprint/document.py b/weasyprint/document.py index aa10c7e3c..91d3dc18d 100644 --- a/weasyprint/document.py +++ b/weasyprint/document.py @@ -607,8 +607,9 @@ def write_pdf(self, target=None, zoom=1, attachments=None): """ # 0.75 = 72 PDF point (cairo units) per inch / 96 CSS pixel per inch scale = zoom * 0.75 - # Use an in-memory buffer. We will need to seek for metadata - # TODO: avoid this if target can seek? Benchmark first. + # Use an in-memory buffer, as we will need to seek for + # metadata. Directly using the target when possible doesn't + # significantly save time and memory use. file_obj = io.BytesIO() # (1, 1) is overridden by .set_size() below. surface = cairo.PDFSurface(file_obj, 1, 1) diff --git a/weasyprint/draw.py b/weasyprint/draw.py index 3ea4441c3..9ddf08277 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -253,17 +253,11 @@ def draw_stacking_context(context, stacking_context, enable_hinting): # Point 7 for block in [box] + stacking_context.blocks_and_cells: - if block.outside_list_marker: - draw_inline_level( - context, stacking_context.page, - block.outside_list_marker, enable_hinting) - if isinstance(block, boxes.ReplacedBox): draw_replacedbox(context, block) else: for child in block.children: if isinstance(child, boxes.LineBox): - # TODO: draw inline tables draw_inline_level( context, stacking_context.page, child, enable_hinting) diff --git a/weasyprint/fonts.py b/weasyprint/fonts.py index 377716c18..d67d59cd2 100644 --- a/weasyprint/fonts.py +++ b/weasyprint/fonts.py @@ -428,7 +428,7 @@ def add_font_face(self, rule_descriptors, url_fetcher): # too as explained in Behdad's blog entry. # TODO: What about pango_fc_font_map_config_changed() # as suggested in Behdad's blog entry? - # Though it seems to work without... + # Though it seems to work without… return filename else: LOGGER.debug('Failed to load font at "%s"', url) diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index cdf766105..b2c57e9d2 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -84,7 +84,6 @@ class Box(object): transformation_matrix = None bookmark_label = None string_set = None - outside_list_marker = None # Default, overriden on some subclasses def all_children(self): @@ -107,12 +106,11 @@ def anonymous_from(cls, parent, *args, **kwargs): def copy(self): """Return shallow copy of the box.""" cls = type(self) - # Create a new instance without calling __init__: initializing - # styles may be kinda expensive, no need to do it again. + # Create a new instance without calling __init__: parameters are + # different depending on the class. new_box = cls.__new__(cls) # Copy attributes new_box.__dict__.update(self.__dict__) - new_box.style = self.style return new_box def translate(self, dx=0, dy=0, ignore_floats=False): @@ -296,15 +294,6 @@ def __init__(self, element_tag, style, children): def all_children(self): return self.children - def enumerate_skip(self, skip_num=0): - """Yield ``(child, child_index)`` tuples for each child. - - ``skip_num`` children are skipped before iterating over them. - - """ - for index in range(skip_num, len(self.children)): - yield index, self.children[index] - def _reset_spacing(self, side): """Set to 0 the margin, padding and border of ``side``.""" self.style['margin_%s' % side] = Dimension(0, 'px') @@ -326,18 +315,20 @@ def _reset_spacing(self, side): def _remove_decoration(self, start, end): if start or end: + old_style = self.style self.style = self.style.copy() if start: self._reset_spacing('top') if end: self._reset_spacing('bottom') + if (start or end) and old_style == self.style: + # Don't copy style if there's no need to, save some memory + self.style = old_style def copy_with_children(self, new_children, is_start=True, is_end=True): """Create a new equivalent box with given ``new_children``.""" new_box = self.copy() new_box.children = tuple(new_children) - if not is_start: - new_box.outside_list_marker = None if self.style['box_decoration_break'] == 'slice': new_box._remove_decoration(not is_start, not is_end) return new_box @@ -400,10 +391,6 @@ class BlockBox(BlockContainerBox, BlockLevelBox): generates a block box. """ - # TODO: remove this when outside list marker are absolute children - def all_children(self): - return (itertools.chain(self.children, [self.outside_list_marker]) - if self.outside_list_marker else self.children) class LineBox(ParentBox): @@ -438,12 +425,16 @@ class InlineLevelBox(Box): """ def _remove_decoration(self, start, end): if start or end: + old_style = self.style self.style = self.style.copy() ltr = self.style['direction'] == 'ltr' if start: self._reset_spacing('left' if ltr else 'right') if end: self._reset_spacing('right' if ltr else 'left') + if (start or end) and old_style == self.style: + # Don't copy style if there's no need to, save some memory + self.style = old_style class InlineBox(InlineLevelBox, ParentBox): diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index 55fd60b2e..e0005bbf5 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -20,7 +20,7 @@ import tinycss2.color3 from .. import html -from ..css import properties +from ..css import computed_values, properties from ..logger import LOGGER from . import boxes, counters @@ -58,8 +58,7 @@ def build_formatting_structure(element_tree, style_for, get_image_from_uri, def root_style_for(element, pseudo_type=None): style = style_for(element, pseudo_type) if style: - # TODO: we should check that the element has a parent instead. - if element.tag == 'html': + if element == element_tree: style['display'] = 'block' else: style['display'] = 'none' @@ -81,7 +80,7 @@ def root_style_for(element, pseudo_type=None): return box -def make_box(element_tag, style, content, get_image_from_uri): +def make_box(element_tag, style, content): return BOX_TYPE_FROM_DISPLAY[style['display']]( element_tag, style, content) @@ -123,7 +122,7 @@ def element_to_box(element, style_for, get_image_from_uri, base_url, if display == 'none': return [] - box = make_box(element.tag, style, [], get_image_from_uri) + box = make_box(element.tag, style, []) if state is None: # use a list to have a shared mutable object @@ -137,10 +136,8 @@ def element_to_box(element, style_for, get_image_from_uri, base_url, update_counters(state, style) + outside_markers = [] children = [] - if display == 'list-item': - children.extend(add_box_marker( - box, counter_values, get_image_from_uri)) # If this element’s direct children create new scopes, the counter # names will be in this new list @@ -149,12 +146,22 @@ def element_to_box(element, style_for, get_image_from_uri, base_url, box.first_letter_style = style_for(element, 'first-letter') box.first_line_style = style_for(element, 'first-line') + if style['display'] == 'list-item': + marker_boxes = marker_to_box( + element, state, style, style_for, get_image_from_uri, + target_collector) + if marker_boxes: + if style['list_style_position'] == 'outside': + outside_markers.extend(marker_boxes) + else: + children.extend(marker_boxes) + children.extend(before_after_to_box( element, 'before', state, style_for, get_image_from_uri, target_collector)) # collect anchor's counter_values, maybe it's a target. - # to get the spec-conform counter_valuse we must do it here, + # to get the spec-conform counter_values we must do it here, # after the ::before is parsed and befor the ::after is if style['anchor']: target_collector.store_target(style['anchor'], counter_values, box) @@ -188,18 +195,33 @@ def element_to_box(element, style_for, get_image_from_uri, base_url, # calculate string-set and bookmark-label set_content_lists(element, box, style, counter_values, target_collector) + if outside_markers and not box.children: + # See https://www.w3.org/TR/css-lists-3/#list-style-position-outside + # + # "The size or contents of the marker box may affect the height of the + # principal block box and/or the height of its first line box, and in + # some cases may cause the creation of a new line box; this + # interaction is also not defined." + # + # We decide here to add a zero-width space to have a minimum + # height. Adding text boxes is not the best idea, but it's not a good + # moment to add an empty line box, and the specification lets us do + # almost what we want, so… + box.children = [boxes.TextBox.anonymous_from(box, '​')] + # Specific handling for the element. (eg. replaced element) - return html.handle_element(element, box, get_image_from_uri, base_url) + return outside_markers + html.handle_element( + element, box, get_image_from_uri, base_url) def before_after_to_box(element, pseudo_type, state, style_for, get_image_from_uri, target_collector): - """Yield the box for ::before or ::after pseudo-element if there is one.""" + """Return the boxes for ::before or ::after pseudo-element.""" style = style_for(element, pseudo_type) if pseudo_type and style is None: # Pseudo-elements with no style at all do not get a style dict. # Their initial content property computes to 'none'. - return + return [] # TODO: should be the computed value. When does the used value for # `display` differ from the computer value? It's at least wrong for @@ -207,24 +229,97 @@ def before_after_to_box(element, pseudo_type, state, style_for, display = style['display'] content = style['content'] if 'none' in (display, content) or content in ('normal', 'inhibit'): - return + return [] - box = make_box( - '%s::%s' % (element.tag, pseudo_type), style, [], get_image_from_uri) + box = make_box('%s::%s' % (element.tag, pseudo_type), style, []) quote_depth, counter_values, _counter_scopes = state update_counters(state, style) children = [] + + outside_markers = [] if display == 'list-item': - children.extend(add_box_marker( - box, counter_values, get_image_from_uri)) + marker_boxes = marker_to_box( + element, state, style, style_for, get_image_from_uri, + target_collector) + if marker_boxes: + if style['list_style_position'] == 'outside': + outside_markers.extend(marker_boxes) + else: + children.extend(marker_boxes) + children.extend(content_to_boxes( style, box, quote_depth, counter_values, get_image_from_uri, target_collector)) box.children = children - yield box + return outside_markers + [box] + + +def marker_to_box(element, state, parent_style, style_for, get_image_from_uri, + target_collector): + """Yield the box for ::marker pseudo-element if there is one. + + https://drafts.csswg.org/css-lists-3/#marker-pseudo + + """ + style = style_for(element, 'marker') + + children = [] + + # TODO: should be the computed value. When does the used value for + # `display` differ from the computer value? It's at least wrong for + # `content` where 'normal' computes as 'inhibit' for pseudo elements. + quote_depth, counter_values, _counter_scopes = state + + box = make_box('%s::marker' % element.tag, style, children) + + if style['display'] == 'none': + return + + image_type, image = style['list_style_image'] + + if style['content'] not in ('normal', 'inhibit'): + children.extend(content_to_boxes( + style, box, quote_depth, counter_values, get_image_from_uri, + target_collector)) + + else: + if image_type == 'url': + # image may be None here too, in case the image is not available. + image = get_image_from_uri(image) + if image is not None: + box = boxes.InlineReplacedBox.anonymous_from(box, image) + children.append(box) + + if not children and style['list_style_type'] != 'none': + counter_value = counter_values.get('list-item', [0])[-1] + # TODO: rtl numbered list has the dot on the left + marker_text = counters.format_list_marker( + counter_value, style['list_style_type']) + box = boxes.TextBox.anonymous_from(box, marker_text) + box.style['white_space'] = 'pre-wrap' + children.append(box) + + if not children: + return + + if parent_style['list_style_position'] == 'outside': + marker_box = boxes.BlockBox.anonymous_from(box, children) + # We can safely edit everything that can't be changed by user style + # See https://drafts.csswg.org/css-pseudo-4/#marker-pseudo + marker_box.style['position'] = 'absolute' + if parent_style['direction'] == 'ltr': + translate_x = properties.Dimension(-100, '%') + else: + translate_x = properties.Dimension(100, '%') + translate_y = computed_values.ZERO_PIXELS + marker_box.style['transform'] = ( + ('translate', (translate_x, translate_y)),) + else: + marker_box = boxes.InlineBox.anonymous_from(box, children) + yield marker_box def _collect_missing_counter(counter_name, counter_values, missing_counters): @@ -433,13 +528,11 @@ def parse_again(mixin_pagebased_counters=None): local_counters.update(parent_box.cached_counter_values) local_children = [] - if style['display'] == 'list-item': - local_children.extend(add_box_marker( - parent_box, local_counters, get_image_from_uri)) local_children.extend(content_to_boxes( style, parent_box, orig_quote_depth, local_counters, get_image_from_uri, target_collector)) + # TODO: do we need to add markers here? # TODO: redo the formatting structure of the parent instead of hacking # the already formatted structure. Find why inline_in_blocks has # sometimes already been called, and sometimes not. @@ -584,51 +677,6 @@ def update_counters(state, style): values[-1] += value -def add_box_marker(box, counter_values, get_image_from_uri): - """Add a list marker to boxes for elements with ``display: list-item``, - and yield children to add a the start of the box. - - See http://www.w3.org/TR/CSS21/generate.html#lists - - """ - style = box.style - image_type, image = style['list_style_image'] - if image_type == 'url': - # surface may be None here too, in case the image is not available. - image = get_image_from_uri(image) - - if image is None: - type_ = style['list_style_type'] - if type_ == 'none': - return - counter_value = counter_values.get('list-item', [0])[-1] - # TODO: rtl numbered list has the dot on the left - marker_text = counters.format_list_marker(counter_value, type_) - marker_box = boxes.TextBox.anonymous_from(box, marker_text) - else: - marker_box = boxes.InlineReplacedBox.anonymous_from(box, image) - marker_box.is_list_marker = True - marker_box.element_tag += '::marker' - - position = style['list_style_position'] - direction = box.style['direction'] - # Apply a margin of 0.5em. The margin to use depends on list-style-position - # and direction. - half_em = 0.5 * box.style['font_size'] - propvalue = properties.Dimension(half_em, 'px') - marker_box.style = marker_box.style.copy() - if position == 'inside' or direction == 'ltr': - marker_box.style['margin_right'] = propvalue - else: - marker_box.style['margin_left'] = propvalue - - if position == 'inside': - # TODO: rtl markers must be the rightmost box of the first line - yield marker_box - elif position == 'outside': - box.outside_list_marker = marker_box - - def is_whitespace(box, _has_non_whitespace=re.compile('\\S').search): """Return True if ``box`` is a TextBox with only whitespace.""" return isinstance(box, boxes.TextBox) and not _has_non_whitespace(box.text) @@ -1407,7 +1455,8 @@ def _inner_block_in_inline(box, skip_stack=None): else: skip, skip_stack = skip_stack - for index, child in box.enumerate_skip(skip): + for i, child in enumerate(box.children[skip:]): + index = i + skip if isinstance(child, boxes.BlockLevelBox) and \ child.is_in_normal_flow(): assert skip_stack is None # Should not skip here diff --git a/weasyprint/formatting_structure/counters.py b/weasyprint/formatting_structure/counters.py index 10769fef2..39187fe67 100644 --- a/weasyprint/formatting_structure/counters.py +++ b/weasyprint/formatting_structure/counters.py @@ -21,7 +21,7 @@ INITIAL_VALUES = dict( negative=('-', ''), prefix='', - suffix='.', + suffix='. ', range=(float('-inf'), float('inf')), fallback='decimal', # type and symbols ommited here. @@ -227,13 +227,13 @@ def additive(symbols, negative, value): 'disc', type='repeating', symbols=['•'], # U+2022, BULLET - suffix='', + suffix=' ', ) register_style( 'circle', type='repeating', symbols=['◦'], # U+25E6 WHITE BULLET - suffix='', + suffix=' ', ) register_style( 'square', @@ -241,7 +241,7 @@ def additive(symbols, negative, value): # CSS Lists 3 suggests U+25FE BLACK MEDIUM SMALL SQUARE # But I think this one looks better. symbols=['▪'], # U+25AA BLACK SMALL SQUARE - suffix='', + suffix=' ', ) register_style( 'lower-latin', diff --git a/weasyprint/layout/absolute.py b/weasyprint/layout/absolute.py index 42b3fa5c7..51076f495 100644 --- a/weasyprint/layout/absolute.py +++ b/weasyprint/layout/absolute.py @@ -206,12 +206,10 @@ def absolute_block(context, box, containing_block, fixed_boxes): # avoid a circular import from .blocks import block_container_layout - # TODO: remove device_size everywhere else new_box, _, _, _, _ = block_container_layout( context, box, max_position_y=float('inf'), skip_stack=None, - device_size=None, page_is_empty=False, - absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, - adjoining_margins=None) + page_is_empty=False, absolute_boxes=absolute_boxes, + fixed_boxes=fixed_boxes, adjoining_margins=None) for child_placeholder in absolute_boxes: absolute_layout(context, child_placeholder, new_box, fixed_boxes) @@ -247,12 +245,10 @@ def absolute_flex(context, box, containing_block_sizes, fixed_boxes, if box.is_table_wrapper: table_wrapper_width(context, box, (cb_width, cb_height)) - # TODO: remove device_size everywhere else new_box, _, _, _, _ = flex_layout( context, box, max_position_y=float('inf'), skip_stack=None, - containing_block=containing_block, device_size=None, - page_is_empty=False, absolute_boxes=absolute_boxes, - fixed_boxes=fixed_boxes) + containing_block=containing_block, page_is_empty=False, + absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes) for child_placeholder in absolute_boxes: absolute_layout(context, child_placeholder, new_box, fixed_boxes) @@ -311,7 +307,7 @@ def absolute_box_layout(context, box, containing_block, fixed_boxes): def absolute_replaced(context, box, containing_block): # avoid a circular import from .inlines import inline_replaced_box_width_height - inline_replaced_box_width_height(box, device_size=None) + inline_replaced_box_width_height(box, containing_block) cb_x, cb_y, cb_width, cb_height = containing_block ltr = box.style['direction'] == 'ltr' diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index e577f753c..e7ea66eb7 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -17,15 +17,14 @@ from .inlines import ( iter_line_boxes, min_max_auto_replaced, replaced_box_height, replaced_box_width) -from .markers import list_marker_layout from .min_max import handle_min_max_width from .percentages import resolve_percentages, resolve_position_percentages from .tables import table_layout, table_wrapper_width def block_level_layout(context, box, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, - absolute_boxes, fixed_boxes, adjoining_margins): + containing_block, page_is_empty, absolute_boxes, + fixed_boxes, adjoining_margins): """Lay out the block-level ``box``. :param max_position_y: the absolute vertical position (as in @@ -61,26 +60,23 @@ def block_level_layout(context, box, max_position_y, skip_stack, return block_level_layout_switch( context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins) + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins) def block_level_layout_switch(context, box, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, - absolute_boxes, fixed_boxes, - adjoining_margins): + containing_block, page_is_empty, absolute_boxes, + fixed_boxes, adjoining_margins): """Call the layout function corresponding to the ``box`` type.""" if isinstance(box, boxes.TableBox): return table_layout( context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes) + page_is_empty, absolute_boxes, fixed_boxes) elif isinstance(box, boxes.BlockBox): return block_box_layout( context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins) + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins) elif isinstance(box, boxes.BlockReplacedBox): - box = block_replaced_box_layout(box, containing_block, device_size) + box = block_replaced_box_layout(box, containing_block) # Don't collide with floats # http://www.w3.org/TR/CSS21/visuren.html#floats box.position_x, box.position_y, _ = avoid_collisions( @@ -93,21 +89,20 @@ def block_level_layout_switch(context, box, max_position_y, skip_stack, elif isinstance(box, boxes.FlexBox): return flex_layout( context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes) + page_is_empty, absolute_boxes, fixed_boxes) else: # pragma: no cover raise TypeError('Layout for %s not handled yet' % type(box).__name__) def block_box_layout(context, box, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, - absolute_boxes, fixed_boxes, adjoining_margins): + containing_block, page_is_empty, absolute_boxes, + fixed_boxes, adjoining_margins): """Lay out the block ``box``.""" if (box.style['column_width'] != 'auto' or box.style['column_count'] != 'auto'): result = columns_layout( context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins) + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins) resume_at = result[1] # TODO: this condition and the whole relayout are probably wrong @@ -120,8 +115,8 @@ def block_box_layout(context, box, max_position_y, skip_stack, max_position_y -= bottom_spacing result = columns_layout( context, box, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, - absolute_boxes, fixed_boxes, adjoining_margins) + containing_block, page_is_empty, absolute_boxes, + fixed_boxes, adjoining_margins) return result elif box.is_table_wrapper: @@ -131,8 +126,8 @@ def block_box_layout(context, box, max_position_y, skip_stack, new_box, resume_at, next_page, adjoining_margins, collapsing_through = \ block_container_layout( - context, box, max_position_y, skip_stack, device_size, - page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins) + context, box, max_position_y, skip_stack, page_is_empty, + absolute_boxes, fixed_boxes, adjoining_margins) if new_box and new_box.is_table_wrapper: # Don't collide with floats # http://www.w3.org/TR/CSS21/visuren.html#floats @@ -144,26 +139,26 @@ def block_box_layout(context, box, max_position_y, skip_stack, @handle_min_max_width -def block_replaced_width(box, containing_block, device_size): +def block_replaced_width(box, containing_block): # http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width - replaced_box_width.without_min_max(box, device_size) + replaced_box_width.without_min_max(box, containing_block) block_level_width.without_min_max(box, containing_block) -def block_replaced_box_layout(box, containing_block, device_size): +def block_replaced_box_layout(box, containing_block): """Lay out the block :class:`boxes.ReplacedBox` ``box``.""" box = box.copy() if box.style['width'] == 'auto' and box.style['height'] == 'auto': computed_margins = box.margin_left, box.margin_right block_replaced_width.without_min_max( - box, containing_block, device_size) - replaced_box_height.without_min_max(box, device_size) + box, containing_block) + replaced_box_height.without_min_max(box) min_max_auto_replaced(box) box.margin_left, box.margin_right = computed_margins block_level_width.without_min_max(box, containing_block) else: - block_replaced_width(box, containing_block, device_size) - replaced_box_height(box, device_size) + block_replaced_width(box, containing_block) + replaced_box_height(box) return box @@ -257,8 +252,8 @@ def relative_positioning(box, containing_block): def block_container_layout(context, box, max_position_y, skip_stack, - device_size, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins=None): + page_is_empty, absolute_boxes, fixed_boxes, + adjoining_margins=None): """Set the ``box`` height.""" # TODO: boxes.FlexBox is allowed here because flex_layout calls # block_container_layout, there's probably a better solution. @@ -324,7 +319,8 @@ def block_container_layout(context, box, max_position_y, skip_stack, else: skip, skip_stack = skip_stack first_letter_style = None - for index, child in box.enumerate_skip(skip): + for i, child in enumerate(box.children[skip:]): + index = i + skip child.position_x = position_x # XXX does not count margins in adjoining_margins: child.position_y = position_y @@ -341,8 +337,7 @@ def block_container_layout(context, box, max_position_y, skip_stack, fixed_boxes.append(placeholder) elif child.is_floated(): new_child = float_layout( - context, child, box, device_size, absolute_boxes, - fixed_boxes) + context, child, box, absolute_boxes, fixed_boxes) # New page if overflow if (page_is_empty and not new_children) or not ( new_child.position_y + new_child.height > @@ -357,7 +352,6 @@ def block_container_layout(context, box, max_position_y, skip_stack, page_break = block_level_page_break( last_in_flow_child, child) if new_children and page_break in ('avoid', 'avoid-page'): - # TODO: fill the blank space at the bottom of the page result = find_earlier_page_break( new_children, absolute_boxes, fixed_boxes) if result: @@ -376,7 +370,7 @@ def block_container_layout(context, box, max_position_y, skip_stack, new_containing_block = box lines_iterator = iter_line_boxes( context, child, position_y, skip_stack, - new_containing_block, device_size, absolute_boxes, fixed_boxes, + new_containing_block, absolute_boxes, fixed_boxes, first_letter_style) is_page_break = False for line, resume_at in lines_iterator: @@ -466,7 +460,6 @@ def block_container_layout(context, box, max_position_y, skip_stack, new_containing_block = box if not new_containing_block.is_table_wrapper: - # TODO: there's no collapsing margins inside tables, right? resolve_percentages(child, new_containing_block) if (child.is_in_normal_flow() and last_in_flow_child is None and @@ -498,7 +491,7 @@ def block_container_layout(context, box, max_position_y, skip_stack, adjoining_margins = [] position_y = box.content_box_y() - if adjoining_margins and isinstance(child, boxes.TableBox): + if adjoining_margins and box.is_table_wrapper: collapsed_margin = collapse_margin(adjoining_margins) child.position_y += collapsed_margin position_y += collapsed_margin @@ -513,10 +506,8 @@ def block_container_layout(context, box, max_position_y, skip_stack, (new_child, resume_at, next_page, next_adjoining_margins, collapsing_through) = block_level_layout( context, child, max_position_y, skip_stack, - new_containing_block, device_size, - page_is_empty_with_no_children, - absolute_boxes, fixed_boxes, - adjoining_margins) + new_containing_block, page_is_empty_with_no_children, + absolute_boxes, fixed_boxes, adjoining_margins) skip_stack = None if new_child is not None: @@ -632,8 +623,11 @@ def block_container_layout(context, box, max_position_y, skip_stack, # not adjoining. (position_y is not used afterwards.) adjoining_margins = [] - if box.border_bottom_width or box.padding_bottom or ( - establishes_formatting_context(box) or box.is_for_root_element): + if (box.border_bottom_width or + box.padding_bottom or + establishes_formatting_context(box) or + box.is_for_root_element or + box.is_table_wrapper): position_y += collapse_margin(adjoining_margins) adjoining_margins = [] @@ -677,24 +671,6 @@ def block_container_layout(context, box, max_position_y, skip_stack, if next_page['page'] is None: next_page['page'] = new_box.page_values()[1] - list_marker_layout(context, new_box) - if new_box.outside_list_marker: - # See https://www.w3.org/TR/css-lists-3/#list-style-position-outside - # - # "The size or contents of the marker box may affect the height of the - # principal block box and/or the height of its first line box, and in - # some cases may cause the creation of a new line box; this - # interaction is also not defined." - # - # We decide here to set the list item box heigth to be at least the - # height of the marker. Adding an empty line would theoretically be a - # cleaner solution, but formatting the formatting structure is tricky - # and we don't need another workaround there. - collapsing_through = False - adjoining_margins = [] - new_box.height = max( - new_box.height, new_box.outside_list_marker.height) - return new_box, resume_at, next_page, adjoining_margins, collapsing_through diff --git a/weasyprint/layout/columns.py b/weasyprint/layout/columns.py index bc28ae867..7061f3386 100644 --- a/weasyprint/layout/columns.py +++ b/weasyprint/layout/columns.py @@ -16,7 +16,7 @@ def columns_layout(context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes, + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins): """Lay out a multi-column ``box``.""" # Avoid circular imports @@ -125,8 +125,8 @@ def create_column_box(children): block.position_y = current_position_y new_child, _, _, adjoining_margins, _ = block_level_layout( context, block, original_max_position_y, skip_stack, - containing_block, device_size, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins) + containing_block, page_is_empty, absolute_boxes, fixed_boxes, + adjoining_margins) new_children.append(new_child) current_position_y = ( new_child.border_height() + new_child.border_box_y()) @@ -142,7 +142,7 @@ def create_column_box(children): column_box = create_column_box(column_children) new_child, _, _, _, _ = block_box_layout( context, column_box, float('inf'), skip_stack, containing_block, - device_size, page_is_empty, [], [], []) + page_is_empty, [], [], []) height = new_child.margin_height() if style['column_fill'] == 'balance': height /= count @@ -156,8 +156,8 @@ def create_column_box(children): # Render the column new_box, resume_at, next_page, _, _ = block_box_layout( context, column_box, box.content_box_y() + height, - column_skip_stack, containing_block, device_size, - page_is_empty, [], [], []) + column_skip_stack, containing_block, page_is_empty, + [], [], []) column_skip_stack = resume_at # Get the empty space at the bottom of the column box @@ -168,8 +168,7 @@ def create_column_box(children): # Get the minimum size needed to render the next box next_box, _, _, _, _ = block_box_layout( context, column_box, box.content_box_y(), - column_skip_stack, containing_block, device_size, True, - [], [], []) + column_skip_stack, containing_block, True, [], [], []) next_box_size = next_box.children[0].margin_height() # Append the size needed to render the next box in this @@ -220,8 +219,8 @@ def create_column_box(children): new_child, column_skip_stack, column_next_page, _, _ = ( block_box_layout( context, column_box, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, - absolute_boxes, fixed_boxes, None)) + containing_block, page_is_empty, absolute_boxes, + fixed_boxes, None)) if new_child is None: break next_page = column_next_page diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py index 3f0dc2e4e..dd6651244 100644 --- a/weasyprint/layout/flex.py +++ b/weasyprint/layout/flex.py @@ -24,7 +24,7 @@ class FlexLine(list): def flex_layout(context, box, max_position_y, skip_stack, containing_block, - device_size, page_is_empty, absolute_boxes, fixed_boxes): + page_is_empty, absolute_boxes, fixed_boxes): # Avoid a circular import from . import blocks, preferred @@ -152,8 +152,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block, new_child.style['max_height'] = Dimension(float('inf'), 'px') new_child = blocks.block_level_layout( context, new_child, float('inf'), child_skip_stack, - parent_box, device_size, page_is_empty, absolute_boxes=[], - fixed_boxes=[], adjoining_margins=[])[0] + parent_box, page_is_empty, [], [], [])[0] content_size = new_child.height child.min_height = min(specified_size, content_size) @@ -212,8 +211,8 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block, new_child.width = float('inf') new_child = blocks.block_level_layout( context, new_child, float('inf'), child_skip_stack, - parent_box, device_size, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins=[])[0] + parent_box, page_is_empty, absolute_boxes, fixed_boxes, + adjoining_margins=[])[0] child.flex_base_size = new_child.margin_height() elif child.style[axis] == 'min-content': child.style[axis] = 'auto' @@ -227,8 +226,8 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block, new_child.width = 0 new_child = blocks.block_level_layout( context, new_child, float('inf'), child_skip_stack, - parent_box, device_size, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins=[])[0] + parent_box, page_is_empty, absolute_boxes, fixed_boxes, + adjoining_margins=[])[0] child.flex_base_size = new_child.margin_height() else: assert child.style[axis].unit == 'px' @@ -461,8 +460,8 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block, new_child, _, _, adjoining_margins, _ = ( blocks.block_level_layout_switch( context, child_copy, float('inf'), child_skip_stack, - parent_box, device_size, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins=[])) + parent_box, page_is_empty, absolute_boxes, fixed_boxes, + adjoining_margins=[])) child._baseline = find_in_flow_baseline(new_child) or 0 if cross == 'height': @@ -835,7 +834,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block, if child.is_flex_item: new_child, child_resume_at = blocks.block_level_layout_switch( context, child, max_position_y, child_skip_stack, box, - device_size, page_is_empty, absolute_boxes, fixed_boxes, + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins=[])[:2] if new_child is None: if resume_at and resume_at[0]: diff --git a/weasyprint/layout/float.py b/weasyprint/layout/float.py index 1bca1e765..c521f4cde 100644 --- a/weasyprint/layout/float.py +++ b/weasyprint/layout/float.py @@ -24,8 +24,7 @@ def float_width(box, context, containing_block): box.width = shrink_to_fit(context, box, containing_block.width) -def float_layout(context, box, containing_block, device_size, absolute_boxes, - fixed_boxes): +def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes): """Set the width and position of floating ``box``.""" # Avoid circular imports from .blocks import block_container_layout @@ -57,7 +56,7 @@ def float_layout(context, box, containing_block, device_size, absolute_boxes, box.position_y += clearance if isinstance(box, boxes.BlockReplacedBox): - inline_replaced_box_width_height(box, device_size=None) + inline_replaced_box_width_height(box, containing_block) elif box.width == 'auto': float_width(box, context, containing_block) @@ -68,7 +67,7 @@ def float_layout(context, box, containing_block, device_size, absolute_boxes, context.create_block_formatting_context() box, _, _, _, _ = block_container_layout( context, box, max_position_y=float('inf'), - skip_stack=None, device_size=device_size, page_is_empty=False, + skip_stack=None, page_is_empty=False, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, adjoining_margins=None) context.finish_block_formatting_context(box) @@ -76,8 +75,8 @@ def float_layout(context, box, containing_block, device_size, absolute_boxes, box, _, _, _, _ = flex_layout( context, box, max_position_y=float('inf'), skip_stack=None, containing_block=containing_block, - device_size=device_size, page_is_empty=False, - absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes) + page_is_empty=False, absolute_boxes=absolute_boxes, + fixed_boxes=fixed_boxes) else: assert isinstance(box, boxes.BlockReplacedBox) diff --git a/weasyprint/layout/inlines.py b/weasyprint/layout/inlines.py index 21878229c..c42c38ca6 100644 --- a/weasyprint/layout/inlines.py +++ b/weasyprint/layout/inlines.py @@ -14,7 +14,7 @@ from ..css import computed_from_cascaded from ..css.computed_values import ex_ratio, strut_layout from ..formatting_structure import boxes -from ..text import can_break_text, split_first_line +from ..text import can_break_text, create_layout, split_first_line from .absolute import AbsolutePlaceholder, absolute_layout from .flex import flex_layout from .float import avoid_collisions, float_layout @@ -22,13 +22,11 @@ from .percentages import resolve_one_percentage, resolve_percentages from .preferred import ( inline_min_content_width, shrink_to_fit, trailing_whitespace_size) -from .replaced import image_marker_layout from .tables import find_in_flow_baseline, table_wrapper_width def iter_line_boxes(context, box, position_y, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes, - first_letter_style): + absolute_boxes, fixed_boxes, first_letter_style): """Return an iterator of ``(line, resume_at)``. ``line`` is a laid-out LineBox with as much content as possible that @@ -41,7 +39,6 @@ def iter_line_boxes(context, box, position_y, skip_stack, containing_block, already laid-out line. :param containing_block: Containing block of the line box: a :class:`BlockContainerBox` - :param device_size: ``(width, height)`` of the current page. """ resolve_percentages(box, containing_block) @@ -53,7 +50,7 @@ def iter_line_boxes(context, box, position_y, skip_stack, containing_block, while 1: line, resume_at = get_next_linebox( context, box, position_y, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes, first_letter_style) + absolute_boxes, fixed_boxes, first_letter_style) if line: position_y = line.position_y + line.height if line is None: @@ -67,8 +64,8 @@ def iter_line_boxes(context, box, position_y, skip_stack, containing_block, def get_next_linebox(context, linebox, position_y, skip_stack, - containing_block, device_size, absolute_boxes, - fixed_boxes, first_letter_style): + containing_block, absolute_boxes, fixed_boxes, + first_letter_style): """Return ``(line, resume_at)``.""" skip_stack = skip_first_whitespace(linebox, skip_stack) if skip_stack == 'continue': @@ -76,13 +73,19 @@ def get_next_linebox(context, linebox, position_y, skip_stack, skip_stack = first_letter_to_box(linebox, skip_stack, first_letter_style) - linebox.width = inline_min_content_width( - context, linebox, skip_stack=skip_stack, first_line=True) - - linebox.height, _ = strut_layout(linebox.style, context) linebox.position_y = position_y + + if context.excluded_shapes: + # Width and height must be calculated to avoid floats + linebox.width = inline_min_content_width( + context, linebox, skip_stack=skip_stack, first_line=True) + linebox.height, _ = strut_layout(linebox.style, context) + else: + # No float, width and height will be set by the lines + linebox.width = linebox.height = 0 position_x, position_y, available_width = avoid_collisions( context, linebox, containing_block, outer=False) + candidate_height = linebox.height excluded_shapes = context.excluded_shapes[:] @@ -101,8 +104,9 @@ def get_next_linebox(context, linebox, position_y, skip_stack, (line, resume_at, preserved_line_break, first_letter, last_letter, float_width) = split_inline_box( context, linebox, position_x, max_x, skip_stack, containing_block, - device_size, line_absolutes, line_fixed, line_placeholders, - waiting_floats, line_children=[]) + line_absolutes, line_fixed, line_placeholders, waiting_floats, + line_children=[]) + linebox.width, linebox.height = line.width, line.height if is_phantom_linebox(line) and not preserved_line_break: line.height = 0 @@ -167,8 +171,8 @@ def get_next_linebox(context, linebox, position_y, skip_stack, for waiting_float in waiting_floats: waiting_float.position_y = waiting_floats_y waiting_float = float_layout( - context, waiting_float, containing_block, device_size, - absolute_boxes, fixed_boxes) + context, waiting_float, containing_block, absolute_boxes, + fixed_boxes) float_children.append(waiting_float) if float_children: line.children += tuple(float_children) @@ -332,10 +336,12 @@ def first_letter_to_box(box, skip_stack, first_letter_style): @handle_min_max_width -def replaced_box_width(box, device_size): +def replaced_box_width(box, containing_block): """ Compute and set the used width for replaced boxes (inline- or block-level) """ + from .blocks import block_level_width + intrinsic_width, intrinsic_height = box.replacement.get_intrinsic_size( box.style['image_resolution'], box.style['font_size']) @@ -351,15 +357,7 @@ def replaced_box_width(box, device_size): box.width = intrinsic_height * box.replacement.intrinsic_ratio else: # Point #3 - # " It is suggested that, if the containing block's width does - # not itself depend on the replaced element's width, then the - # used value of 'width' is calculated from the constraint - # equation used for block-level, non-replaced elements in - # normal flow. " - # Whaaaaat? Let's not do this and use a value that may work - # well at least with inline blocks. - box.width = ( - box.style['font_size'] * box.replacement.intrinsic_ratio) + block_level_width(box, containing_block) if box.width == 'auto': if box.replacement.intrinsic_ratio is not None: @@ -370,12 +368,12 @@ def replaced_box_width(box, device_size): box.width = intrinsic_width else: # Point #5 - device_width, _device_height = device_size - box.width = min(300, device_width) + # It's pretty useless to rely on device size to set width. + box.width = 300 @handle_min_max_height -def replaced_box_height(box, device_size): +def replaced_box_height(box): """ Compute and set the used height for replaced boxes (inline- or block-level) """ @@ -398,26 +396,26 @@ def replaced_box_height(box, device_size): elif box.height == 'auto' and intrinsic_height is not None: box.height = intrinsic_height elif box.height == 'auto': - device_width, _device_height = device_size - box.height = min(150, device_width / 2) + # It's pretty useless to rely on device size to set width. + box.height = 150 -def inline_replaced_box_layout(box, device_size): +def inline_replaced_box_layout(box, containing_block): """Lay out an inline :class:`boxes.ReplacedBox` ``box``.""" for side in ['top', 'right', 'bottom', 'left']: if getattr(box, 'margin_' + side) == 'auto': setattr(box, 'margin_' + side, 0) - inline_replaced_box_width_height(box, device_size) + inline_replaced_box_width_height(box, containing_block) -def inline_replaced_box_width_height(box, device_size): +def inline_replaced_box_width_height(box, containing_block): if box.style['width'] == 'auto' and box.style['height'] == 'auto': - replaced_box_width.without_min_max(box, device_size) - replaced_box_height.without_min_max(box, device_size) + replaced_box_width.without_min_max(box, containing_block) + replaced_box_height.without_min_max(box) min_max_auto_replaced(box) else: - replaced_box_width(box, device_size) - replaced_box_height(box, device_size) + replaced_box_width(box, containing_block) + replaced_box_height(box) def min_max_auto_replaced(box): @@ -479,14 +477,11 @@ def min_max_auto_replaced(box): def atomic_box(context, box, position_x, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes): + absolute_boxes, fixed_boxes): """Compute the width and the height of the atomic ``box``.""" if isinstance(box, boxes.ReplacedBox): box = box.copy() - if getattr(box, 'is_list_marker', False): - image_marker_layout(box) - else: - inline_replaced_box_layout(box, device_size) + inline_replaced_box_layout(box, containing_block) box.baseline = box.margin_height() elif isinstance(box, boxes.InlineBlockBox): if box.is_table_wrapper: @@ -495,15 +490,14 @@ def atomic_box(context, box, position_x, skip_stack, containing_block, (containing_block.width, containing_block.height)) box = inline_block_box_layout( context, box, position_x, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes) + absolute_boxes, fixed_boxes) else: # pragma: no cover raise TypeError('Layout for %s not handled yet' % type(box).__name__) return box def inline_block_box_layout(context, box, position_x, skip_stack, - containing_block, device_size, absolute_boxes, - fixed_boxes): + containing_block, absolute_boxes, fixed_boxes): # Avoid a circular import from .blocks import block_container_layout @@ -526,8 +520,8 @@ def inline_block_box_layout(context, box, position_x, skip_stack, box.position_y = 0 box, _, _, _, _ = block_container_layout( context, box, max_position_y=float('inf'), skip_stack=skip_stack, - device_size=device_size, page_is_empty=True, - absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes) + page_is_empty=True, absolute_boxes=absolute_boxes, + fixed_boxes=fixed_boxes) box.baseline = inline_block_baseline(box) return box @@ -561,9 +555,8 @@ def inline_block_width(box, context, containing_block): def split_inline_level(context, box, position_x, max_x, skip_stack, - containing_block, device_size, absolute_boxes, - fixed_boxes, line_placeholders, waiting_floats, - line_children): + containing_block, absolute_boxes, fixed_boxes, + line_placeholders, waiting_floats, line_children): """Fit as much content as possible from an inline-level box in a width. Return ``(new_box, resume_at, preserved_line_break, first_letter, @@ -610,12 +603,12 @@ def split_inline_level(context, box, position_x, max_x, skip_stack, (new_box, resume_at, preserved_line_break, first_letter, last_letter, float_widths) = split_inline_box( context, box, position_x, max_x, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes, line_placeholders, - waiting_floats, line_children) + absolute_boxes, fixed_boxes, line_placeholders, waiting_floats, + line_children) elif isinstance(box, boxes.AtomicInlineLevelBox): new_box = atomic_box( context, box, position_x, skip_stack, containing_block, - device_size, absolute_boxes, fixed_boxes) + absolute_boxes, fixed_boxes) new_box.position_x = position_x resume_at = None preserved_line_break = False @@ -631,7 +624,7 @@ def split_inline_level(context, box, position_x, max_x, skip_stack, setattr(box, 'margin_' + side, 0) new_box, resume_at, _, _, _ = flex_layout( context, box, float('inf'), skip_stack, containing_block, - device_size, False, absolute_boxes, fixed_boxes) + False, absolute_boxes, fixed_boxes) preserved_line_break = False first_letter = '\u2e80' last_letter = '\u2e80' @@ -643,9 +636,8 @@ def split_inline_level(context, box, position_x, max_x, skip_stack, def split_inline_box(context, box, position_x, max_x, skip_stack, - containing_block, device_size, absolute_boxes, - fixed_boxes, line_placeholders, waiting_floats, - line_children): + containing_block, absolute_boxes, fixed_boxes, + line_placeholders, waiting_floats, line_children): """Same behavior as split_inline_level.""" # In some cases (shrink-to-fit result being the preferred width) @@ -681,8 +673,8 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, else: skip, skip_stack = skip_stack - box_children = list(box.enumerate_skip(skip)) - for i, (index, child) in enumerate(box_children): + for i, child in enumerate(box.children[skip:]): + index = i + skip child.position_y = box.position_y if child.is_absolutely_positioned(): child.position_x = position_x @@ -696,8 +688,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, continue elif child.is_floated(): child.position_x = position_x - float_width = shrink_to_fit( - context, child, containing_block.width) + float_width = shrink_to_fit(context, child, containing_block.width) # To retrieve the real available space for floats, we must remove # the trailing whitespaces from the line @@ -714,8 +705,8 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, waiting_floats.append(child) else: child = float_layout( - context, child, containing_block, device_size, - absolute_boxes, fixed_boxes) + context, child, containing_block, absolute_boxes, + fixed_boxes) waiting_children.append((index, child)) # Translate previous line children @@ -743,13 +734,13 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, float_resume_at = index + 1 continue - last_child = (i == len(box_children) - 1) + last_child = (index == len(box.children) - 1) available_width = max_x child_waiting_floats = [] new_child, resume_at, preserved, first, last, new_float_widths = ( split_inline_level( context, child, position_x, available_width, skip_stack, - containing_block, device_size, absolute_boxes, fixed_boxes, + containing_block, absolute_boxes, fixed_boxes, line_placeholders, child_waiting_floats, line_children)) if last_child and right_spacing and resume_at is None: # TODO: we should take care of children added into absolute_boxes, @@ -761,7 +752,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, new_child, resume_at, preserved, first, last, new_float_widths = ( split_inline_level( context, child, position_x, available_width, skip_stack, - containing_block, device_size, absolute_boxes, fixed_boxes, + containing_block, absolute_boxes, fixed_boxes, line_placeholders, child_waiting_floats, line_children)) if box.style['direction'] == 'rtl': @@ -835,8 +826,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack, child_new_child, child_resume_at, _, _, _, _ = ( split_inline_level( context, child, child.position_x, max_x, - None, box, device_size, - absolute_boxes, fixed_boxes, + None, box, absolute_boxes, fixed_boxes, line_placeholders, waiting_floats, line_children)) @@ -1130,7 +1120,6 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y): one_ex = box.style['font_size'] * ex_ratio(box.style) top = baseline_y - (one_ex + child.margin_height()) / 2. child_baseline_y = top + child.baseline - # TODO: actually implement vertical-align: top and bottom elif vertical_align == 'text-top': # align top with the top of the parent’s content area top = (baseline_y - box.baseline + box.margin_top + @@ -1142,6 +1131,7 @@ def inline_box_verticality(box, top_bottom_subtrees, baseline_y): box.border_top_width + box.padding_top + box.height) child_baseline_y = bottom - child.margin_height() + child.baseline elif vertical_align in ('top', 'bottom'): + # TODO: actually implement vertical-align: top and bottom # Later, we will assume for this subtree that its baseline # is at y=0. child_baseline_y = 0 @@ -1243,10 +1233,10 @@ def add_word_spacing(context, box, justification_spacing, x_advance): box.position_x += x_advance nb_spaces = count_spaces(box) if nb_spaces > 0: - layout, _, resume_at, _, _, _ = split_first_line( + layout = create_layout( box.text, box.style, context, float('inf'), box.justification_spacing) - assert resume_at is None + layout.deactivate() extra_space = justification_spacing * nb_spaces x_advance += extra_space box.width += extra_space diff --git a/weasyprint/layout/markers.py b/weasyprint/layout/markers.py deleted file mode 100644 index c7d9388f1..000000000 --- a/weasyprint/layout/markers.py +++ /dev/null @@ -1,66 +0,0 @@ -""" - weasyprint.layout.markers - ------------------------- - - Layout for list markers (for ``display: list-item``). - - :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS. - :license: BSD, see LICENSE for details. - -""" - -from ..formatting_structure import boxes -from ..text import split_first_line -from .percentages import resolve_percentages -from .replaced import image_marker_layout -from .tables import find_in_flow_baseline - - -def list_marker_layout(context, box): - """Lay out the list markers of ``box``.""" - # List markers can be either 'inside' or 'outside'. - # Inside markers are layed out just like normal inline content, but - # outside markers need specific layout. - # TODO: implement outside markers in terms of absolute positioning, - # see CSS3 lists. - marker = box.outside_list_marker - if marker: - # Make a copy to ensure unique markers when the marker's layout has - # already been done. - # TODO: this should be done in layout_fixed_boxes - if hasattr(marker, 'position_x'): - marker = box.outside_list_marker = marker.copy() - - resolve_percentages(marker, containing_block=box) - if isinstance(marker, boxes.TextBox): - (marker.pango_layout, _, _, marker.width, marker.height, - marker.baseline) = split_first_line( - marker.text, marker.style, context, max_width=None, - justification_spacing=marker.justification_spacing) - baseline = find_in_flow_baseline(box) - else: - # Image marker - image_marker_layout(marker) - - if isinstance(marker, boxes.TextBox) and baseline: - # Align the baseline of the marker box with the baseline of the - # first line of its list-item’s content-box. - marker.position_y = baseline - marker.baseline - else: - # Align the top of the marker box with the top of its list-item’s - # content-box. - marker.position_y = box.content_box_y() - - # ... and its right with the left of its list-item’s padding box. - # (Swap left and right for right-to-left text.) - marker.position_x = box.border_box_x() - - direction = box.style['direction'] - if direction == 'ltr': - marker.position_x -= marker.margin_width() - else: - # Move to the right margin. - marker.position_x += box.border_width() - if isinstance(marker, boxes.TextBox): - # Take margin/padding into account. - marker.position_x += marker.margin_width() - (marker.width) diff --git a/weasyprint/layout/pages.py b/weasyprint/layout/pages.py index 65a5492a4..60a0800e4 100644 --- a/weasyprint/layout/pages.py +++ b/weasyprint/layout/pages.py @@ -439,8 +439,7 @@ def margin_box_content_layout(context, page, box): box, resume_at, next_page, _, _ = block_container_layout( context, box, max_position_y=float('inf'), skip_stack=None, - device_size=page.style['size'], page_is_empty=True, - absolute_boxes=[], fixed_boxes=[]) + page_is_empty=True, absolute_boxes=[], fixed_boxes=[]) assert resume_at is None vertical_align = box.style['vertical_align'] @@ -550,8 +549,8 @@ def make_page(context, root_box, page_type, resume_at, page_number, positioned_boxes = [] # Mixed absolute and fixed root_box, resume_at, next_page, _, _ = block_level_layout( context, root_box, page_content_bottom, resume_at, - initial_containing_block, device_size, page_is_empty, - positioned_boxes, positioned_boxes, adjoining_margins) + initial_containing_block, page_is_empty, positioned_boxes, + positioned_boxes, adjoining_margins) assert root_box page.fixed_boxes = [ diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index a43ac1afb..4e3d28ae4 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -257,7 +257,7 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip = 0 else: skip, skip_stack = skip_stack - for index, child in box.enumerate_skip(skip): + for child in box.children[skip:]: if child.is_absolutely_positioned(): continue # Skip diff --git a/weasyprint/layout/replaced.py b/weasyprint/layout/replaced.py index beeabe0a8..f989fa3be 100644 --- a/weasyprint/layout/replaced.py +++ b/weasyprint/layout/replaced.py @@ -13,22 +13,6 @@ from .percentages import percentage -def image_marker_layout(box): - """Layout the :class:`boxes.ImageMarkerBox` ``box``. - - :class:`boxes.ImageMarkerBox` objects are :class:`boxes.ReplacedBox` - objects, but their used size is computed differently. - - """ - image = box.replacement - one_em = box.style['font_size'] - iwidth, iheight = image.get_intrinsic_size( - box.style['image_resolution'], one_em) - box.width, box.height = default_image_sizing( - iwidth, iheight, image.intrinsic_ratio, box.width, box.height, - default_width=one_em, default_height=one_em) - - def default_image_sizing(intrinsic_width, intrinsic_height, intrinsic_ratio, specified_width, specified_height, default_width, default_height): diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index c81d77b78..fd66e9c32 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -15,9 +15,8 @@ from .preferred import max_content_width, table_and_columns_preferred_widths -def table_layout(context, table, max_position_y, skip_stack, - containing_block, device_size, page_is_empty, absolute_boxes, - fixed_boxes): +def table_layout(context, table, max_position_y, skip_stack, containing_block, + page_is_empty, absolute_boxes, fixed_boxes): """Layout for a table box.""" # Avoid a circular import from .blocks import block_container_layout @@ -77,7 +76,8 @@ def group_layout(group, position_y, max_position_y, else: skip, skip_stack = skip_stack assert not skip_stack # No breaks inside rows for now - for index_row, row in group.enumerate_skip(skip): + for i, row in enumerate(group.children[skip:]): + index_row = i + skip resolve_percentages(row, containing_block=table) row.position_x = rows_x row.position_y = position_y @@ -122,7 +122,6 @@ def group_layout(group, position_y, max_position_y, context, cell, max_position_y=float('inf'), skip_stack=None, - device_size=device_size, page_is_empty=True, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes) @@ -251,7 +250,8 @@ def body_groups_layout(skip_stack, position_y, max_position_y, skip, skip_stack = skip_stack new_table_children = [] resume_at = None - for index_group, group in table.enumerate_skip(skip): + for i, group in enumerate(table.children[skip:]): + index_group = i + skip if group.is_header or group.is_footer: continue new_group, resume_at = group_layout( diff --git a/weasyprint/tests/test_boxes.py b/weasyprint/tests/test_boxes.py index b90c79a2c..d14429c96 100644 --- a/weasyprint/tests/test_boxes.py +++ b/weasyprint/tests/test_boxes.py @@ -585,7 +585,6 @@ def test_tables_5(): @assert_no_logs def test_tables_6(): - # TODO: re-enable this once we support inline-table # Rule 3.2 assert_tree(parse_all(''), [ ('body', 'Line', [ @@ -940,30 +939,38 @@ def test_counters_2(): ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '1.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '2.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '2. ')])])]), ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '3.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '3. ')])])]), ('li', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '4.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '4. ')])])]), ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '1.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '1.')])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '2.')])])])]), + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '2. ')])])])])]), ('li', 'Block', [ ('li', 'Line', [ - ('li::marker', 'Text', '5.')])])])]) + ('li::marker', 'Inline', [ + ('li::marker', 'Text', '5. ')])])])])]) @assert_no_logs @@ -981,16 +988,20 @@ def test_counters_3(): ('div', 'Block', [ ('p', 'Block', [ ('p', 'Line', [ - ('p::marker', 'Text', '1.')])]), + ('p::marker', 'Inline', [ + ('p::marker', 'Text', '1. ')])])]), ('p', 'Block', [ ('p', 'Line', [ - ('p::marker', 'Text', '2.')])]), + ('p::marker', 'Inline', [ + ('p::marker', 'Text', '2. ')])])]), ('p', 'Block', [ ('p', 'Line', [ - ('p::marker', 'Text', '-55.')])])]), + ('p::marker', 'Inline', [ + ('p::marker', 'Text', '-55. ')])])])]), ('p', 'Block', [ ('p', 'Line', [ - ('p::marker', 'Text', '1.')])])]) + ('p::marker', 'Inline', [ + ('p::marker', 'Text', '1. ')])])])]) @assert_no_logs @@ -1070,7 +1081,8 @@ def test_counters_6(): display: list-item; list-style: inside decimal">'''), [ ('p', 'Block', [ ('p', 'Line', [ - ('p::marker', 'Text', '0.')])])]) + ('p::marker', 'Inline', [ + ('p::marker', 'Text', '0. ')])])])]) @assert_no_logs @@ -1092,6 +1104,23 @@ def test_counters_7(): for counter in '2.0 2.3 4.3'.split()]) +@assert_no_logs +def test_counters_8(): + assert_tree(parse_all(''' + +

+

'''), 2 * [ + ('p', 'Block', [ + ('p::marker', 'Block', [ + ('p::marker', 'Line', [ + ('p::marker', 'Text', '• ')])]), + ('p::before', 'Block', [ + ('p::before', 'Line', [ + ('p::before', 'Text', 'a')])])])]) + + @assert_no_logs def test_counter_styles_1(): assert_tree(parse_all(''' @@ -1717,3 +1746,16 @@ def test_border_collapse_5(): [None, black_3, black_3], [black_3, black_3, black_3], ] + + +@assert_no_logs +@pytest.mark.parametrize('html', ( + '', + 'abc', + '

abc', + '

abc', +)) +def test_display_none_root(html): + box = parse_all(html) + assert box.style['display'] == 'block' + assert not box.children diff --git a/weasyprint/tests/test_css.py b/weasyprint/tests/test_css.py index 5046675d0..dcb38bd27 100644 --- a/weasyprint/tests/test_css.py +++ b/weasyprint/tests/test_css.py @@ -161,8 +161,8 @@ def test_annotate_document(): assert after['border_top_width'] == 42 assert after['border_bottom_width'] == 3 - # TODO much more tests here: test that origin and selector precedence - # and inheritance are correct, ... + # TODO: much more tests here: test that origin and selector precedence + # and inheritance are correct… @assert_no_logs @@ -303,9 +303,9 @@ def test_page_selectors(style, selectors): ('::lipsum { margin: 2cm', ['WARNING: Invalid or unsupported selector']), ('foo { margin-color: red', ['WARNING: Ignored', 'unknown property']), ('foo { margin-top: red', ['WARNING: Ignored', 'invalid value']), - ('@import "relative-uri.css', + ('@import "relative-uri.css"', ['ERROR: Relative URI reference without a base URI']), - ('@import "invalid-protocol://absolute-URL', + ('@import "invalid-protocol://absolute-URL"', ['ERROR: Failed to load stylesheet at']), )) def test_warnings(source, messages): diff --git a/weasyprint/tests/test_css_validation.py b/weasyprint/tests/test_css_validation.py index 098f16dad..b0173cb3c 100644 --- a/weasyprint/tests/test_css_validation.py +++ b/weasyprint/tests/test_css_validation.py @@ -572,7 +572,6 @@ def test_font(): assert expand_to_dict( 'font: small-caps condensed normal 700 large serif' ) == { - # 'font_style': 'normal', XXX shouldn’t this be here? 'font_stretch': 'condensed', 'font_variant_caps': 'small-caps', 'font_weight': 700, @@ -587,6 +586,10 @@ def test_font(): assert_invalid('font: 12px') assert_invalid('font: 12px/foo serif') assert_invalid('font: 12px "Invalid" family') + assert_invalid('font: normal normal normal normal normal large serif') + assert_invalid('font: normal small-caps italic 700 condensed large serif') + assert_invalid('font: small-caps italic 700 normal condensed large serif') + assert_invalid('font: small-caps italic 700 condensed normal large serif') @assert_no_logs diff --git a/weasyprint/tests/test_draw/test_list.py b/weasyprint/tests/test_draw/test_list.py index 3f227e046..b295f9599 100644 --- a/weasyprint/tests/test_draw/test_list.py +++ b/weasyprint/tests/test_draw/test_list.py @@ -25,10 +25,10 @@ ''' ____________ ____________ - __rBBB______ - __BBBB______ - __BBBB______ - __BBBB______ + ___rBBB_____ + ___BBBB_____ + ___BBBB_____ + ___BBBB_____ ____________ ____________ ____________ diff --git a/weasyprint/tests/test_draw/test_transform.py b/weasyprint/tests/test_draw/test_transform.py index 58c0cce5c..4e843ac39 100644 --- a/weasyprint/tests/test_draw/test_transform.py +++ b/weasyprint/tests/test_draw/test_transform.py @@ -218,7 +218,7 @@ def test_2d_transform_10(): @@ -266,7 +266,7 @@ def test_2d_transform_12(): diff --git a/weasyprint/tests/test_layout/test_image.py b/weasyprint/tests/test_layout/test_image.py index 19d60386f..6d9d818ef 100644 --- a/weasyprint/tests/test_layout/test_image.py +++ b/weasyprint/tests/test_layout/test_image.py @@ -302,6 +302,26 @@ def test_images_16(): assert img.height == 200 +@assert_no_logs +def test_images_17(): + page, = parse(''' +

+ ''') + html, = page.children + body, = html.children + div, = body.children + line, = div.children + img, = line.children + assert div.width == 300 + assert div.height == 300 + assert img.element_tag == 'img' + assert img.width == 300 + assert img.height == 150 + + @assert_no_logs def test_linear_gradient(): red = (1, 0, 0, 1) diff --git a/weasyprint/tests/test_layout/test_list.py b/weasyprint/tests/test_layout/test_list.py index 2dda2853f..ba73ea377 100644 --- a/weasyprint/tests/test_layout/test_list.py +++ b/weasyprint/tests/test_layout/test_list.py @@ -18,9 +18,9 @@ @assert_no_logs @pytest.mark.parametrize('inside', ('inside', '',)) @pytest.mark.parametrize('style, character', ( - ('circle', '◦'), - ('disc', '•'), - ('square', '▪'), + ('circle', '◦ '), + ('disc', '• '), + ('square', '▪ '), )) def test_lists_style(inside, style, character): page, = parse(''' @@ -35,18 +35,20 @@ def test_lists_style(inside, style, character): html, = page.children body, = html.children unordered_list, = body.children - list_item, = unordered_list.children if inside: + list_item, = unordered_list.children line, = list_item.children marker, content = line.children + marker_text, = marker.children else: - marker = list_item.outside_list_marker - assert marker.position_x == ( - list_item.padding_box_x() - marker.width - marker.margin_right) + marker, list_item = unordered_list.children + assert marker.position_x == list_item.position_x assert marker.position_y == list_item.position_y line, = list_item.children content, = line.children - assert marker.text == character + marker_line, = marker.children + marker_text, = marker_line.children + assert marker_text.text == character assert content.text == 'abc' @@ -66,5 +68,5 @@ def test_lists_empty_item(): html, = page.children body, = html.children unordered_list, = body.children - li1, li2, li3 = unordered_list.children + _1, li1, _2, li2, _3, li3 = unordered_list.children assert li1.position_y != li2.position_y != li3.position_y diff --git a/weasyprint/tests/test_layout/test_position.py b/weasyprint/tests/test_layout/test_position.py index 6e97ccf1e..10d8ffd23 100644 --- a/weasyprint/tests/test_layout/test_position.py +++ b/weasyprint/tests/test_layout/test_position.py @@ -339,19 +339,21 @@ def test_fixed_positioning_regression_1(): html, = page_1.children body, = html.children ul, img, div, article = body.children + marker = ul.children[0] assert (ul.position_x, ul.position_y) == (80, 10) assert (img.position_x, img.position_y) == (60, 10) assert (div.position_x, div.position_y) == (40, 10) assert (article.position_x, article.position_y) == (0, 0) - assert 60 < ul.children[0].outside_list_marker.position_x < 70 + assert marker.position_x == ul.position_x html, = page_2.children ul, img, div, body = html.children + marker = ul.children[0] assert (ul.position_x, ul.position_y) == (180, 10) assert (img.position_x, img.position_y) == (160, 10) assert (div.position_x, div.position_y) == (140, 10) assert (article.position_x, article.position_y) == (0, 0) - assert 160 < ul.children[0].outside_list_marker.position_x < 170 + assert marker.position_x == ul.position_x @assert_no_logs diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index 66093a8b0..4427187b9 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -2104,3 +2104,61 @@ def test_inline_table_baseline(vertical_align, table_position_y): assert text1.position_y == text2.position_y == 0 assert table.height == 10 * 2 assert table.position_y == table_position_y + + +@assert_no_logs +def test_table_caption_margin_top(): + page, = render_pages(''' + +

+ + + + + +
+

+ ''') + html, = page.children + body, = html.children + h1, wrapper, h2 = body.children + caption, table = wrapper.children + tbody, = table.children + assert (h1.content_box_x(), h1.content_box_y()) == (20, 20) + assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50) + assert (caption.content_box_x(), caption.content_box_y()) == (40, 70) + assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 100) + assert (h2.content_box_x(), h2.content_box_y()) == (20, 130) + + +@assert_no_logs +def test_table_caption_margin_bottom(): + page, = render_pages(''' + +

+ + + + + +
+

+ ''') + html, = page.children + body, = html.children + h1, wrapper, h2 = body.children + table, caption = wrapper.children + tbody, = table.children + assert (h1.content_box_x(), h1.content_box_y()) == (20, 20) + assert (wrapper.content_box_x(), wrapper.content_box_y()) == (20, 50) + assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50) + assert (caption.content_box_x(), caption.content_box_y()) == (40, 80) + assert (h2.content_box_x(), h2.content_box_y()) == (20, 130) diff --git a/weasyprint/tests/test_presentational_hints.py b/weasyprint/tests/test_presentational_hints.py index 294fe2537..cbfa0e4c7 100644 --- a/weasyprint/tests/test_presentational_hints.py +++ b/weasyprint/tests/test_presentational_hints.py @@ -129,8 +129,8 @@ def test_ph_lists(): html, = page._page_box.children body, = html.children ol, ul = body.children - oli1, oli2, oli3, oli4, oli5 = ol.children - uli1, uli2, uli3 = ul.children + _1, oli1, _2, oli2, _3, oli3, _4, oli4, _5, oli5 = ol.children + _1, uli1, _2, uli2, _3, uli3 = ul.children assert oli1.style['list_style_type'] == 'upper-alpha' assert oli2.style['list_style_type'] == 'decimal' assert oli3.style['list_style_type'] == 'lower-alpha' diff --git a/weasyprint/tests/test_unicode.py b/weasyprint/tests/test_unicode.py index 9a8e3f9dd..a00e240ae 100644 --- a/weasyprint/tests/test_unicode.py +++ b/weasyprint/tests/test_unicode.py @@ -53,7 +53,6 @@ def test_unicode(): ) fd.write(html_content.encode('utf8')) - # TODO: change this back to actually read from a file document = FakeHTML(html, encoding='utf8') lines = document_to_pixels(document, 'unicode', 200, 50) assert_pixels_equal('unicode', 200, 50, lines, expected_lines) diff --git a/weasyprint/text.py b/weasyprint/text.py index f57fc2d71..24681e585 100644 --- a/weasyprint/text.py +++ b/weasyprint/text.py @@ -608,19 +608,25 @@ def first_line_metrics(first_line, text, layout, resume_at, space_collapse, length -= len(hyphenation_character.encode('utf8')) elif resume_at: # Set an infinite width as we don't want to break lines when drawing, - # the lines have already been split and the size may differ. + # the lines have already been split and the size may differ. Rendering + # is also much faster when no width is set. pango.pango_layout_set_width(layout.layout, -1) + # Create layout with final text first_line_text = utf8_slice(text, slice(length)) + # Remove trailing spaces if spaces collapse if space_collapse: first_line_text = first_line_text.rstrip(' ') + # Remove soft hyphens layout.set_text(first_line_text.replace('\u00ad', '')) + first_line, _ = layout.get_first_line() length = first_line.length if first_line is not None else 0 - soft_hyphens = 0 + if '\u00ad' in first_line_text: + soft_hyphens = 0 if first_line_text[0] == '\u00ad': length += 2 # len('\u00ad'.encode('utf8')) for i in range(len(layout.text)): @@ -629,7 +635,8 @@ def first_line_metrics(first_line, text, layout, resume_at, space_collapse, soft_hyphens += 1 else: break - length += soft_hyphens * 2 # len('\u00ad'.encode('utf8')) + length += soft_hyphens * 2 # len('\u00ad'.encode('utf8')) + width, height = get_size(first_line, style) baseline = units_to_double(pango.pango_layout_get_baseline(layout.layout)) layout.deactivate() @@ -652,18 +659,18 @@ def setup(self, context, font_size, style): # See https://github.com/Kozea/WeasyPrint/pull/599 if font_size == 0 and ZERO_FONTSIZE_CRASHES_CAIRO: font_size = 1 + hinting = context.enable_hinting if context else False self.layout = ffi.gc( pangocairo.pango_cairo_create_layout(ffi.cast( 'cairo_t *', CAIRO_DUMMY_CONTEXT[hinting]._pointer)), gobject.g_object_unref) + pango_context = pango.pango_layout_get_context(self.layout) if context and context.font_config.font_map: pango.pango_context_set_font_map( pango_context, context.font_config.font_map) - self.font = ffi.gc( - pango.pango_font_description_new(), - pango.pango_font_description_free) + if style['font_language_override'] != 'normal': lang_p, lang = unicode_to_char_p(LST_TO_ISO.get( style['font_language_override'].lower(), @@ -679,6 +686,9 @@ def setup(self, context, font_size, style): assert not isinstance(style['font_family'], str), ( 'font_family should be a list') + self.font = ffi.gc( + pango.pango_font_description_new(), + pango.pango_font_description_free) family_p, family = unicode_to_char_p(','.join(style['font_family'])) pango.pango_font_description_set_family(self.font, family_p) pango.pango_font_description_set_style( @@ -793,7 +803,7 @@ def set_tabs(self): width = int(round(width)) else: width = int(self.style['tab_size'].value) - # TODO: 0 is not handled correctly by Pango + # 0 is not handled correctly by Pango array = ffi.gc( pango.pango_tab_array_new_with_positions( 1, True, pango.PANGO_TAB_LEFT, width or 1), @@ -801,10 +811,7 @@ def set_tabs(self): pango.pango_layout_set_tabs(self.layout, array) def deactivate(self): - del self.layout - del self.font - del self.language - del self.style + del self.layout, self.font, self.language, self.style def reactivate(self, style): self.setup(self.context, style['font_size'], style) @@ -1207,16 +1214,6 @@ def split_first_line(text, style, context, max_width, justification_spacing, pango.pango_layout_set_width( layout.layout, units_from_double(max_width)) layout.set_wrap(PANGO_WRAP_MODE['WRAP_CHAR']) - _, temp_index = layout.get_first_line() - temp_index = temp_index or len(text.encode('utf-8')) - # TODO: WRAP_CHAR is said to "wrap lines at character boundaries", but - # it doesn't. Looks like it tries to split at word boundaries and then - # at character boundaries if there's no enough space for a full word, - # just as WRAP_WORD_CHAR does. That's why we have to split this text - # twice. Find why. It may be related to the problem described in the - # link given in step #3. - first_line_text = utf8_slice(text, slice(temp_index)) - layout.set_text(first_line_text) first_line, index = layout.get_first_line() resume_at = index or first_line.length if resume_at >= len(text.encode('utf-8')):