LibWeb: Support inserting non-inline elements into inline elements

Our layout tree requires that all containers either have inline or
non-inline children. In order to support the layout of non-inline
elements inside inline elements, we need to do a bit of tree
restructuring. It effectively simulates temporarily closing all inline
nodes, appending the block element, and resumes appending to the last
open inline node.

The acid1.txt expectation needed to be updated to reflect the fact that
we now hoist its <p> elements out of the inline <form> they were in.
Visually, the before and after situations for acid1.html are identical.
This commit is contained in:
Jelle Raaijmakers 2025-01-15 16:37:30 +01:00 committed by Jelle Raaijmakers
commit 336684bc5c
Notes: github-actions[bot] 2025-01-23 08:34:24 +00:00
18 changed files with 520 additions and 145 deletions

View file

@ -210,7 +210,6 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho
nonbox_color_on, nonbox_color_on,
identifier, identifier,
color_off); color_off);
builder.append("\n"sv);
} else { } else {
auto& box = as<Layout::Box>(layout_node); auto& box = as<Layout::Box>(layout_node);
StringView color_on = is<Layout::SVGBox>(box) ? svg_box_color_on : box_color_on; StringView color_on = is<Layout::SVGBox>(box) ? svg_box_color_on : box_color_on;
@ -334,10 +333,14 @@ void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool sho
} }
} }
} }
builder.append("\n"sv);
} }
if (is<Layout::NodeWithStyleAndBoxModelMetrics>(layout_node)
&& static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(layout_node).continuation_of_node())
builder.append(" continuation"sv);
builder.append("\n"sv);
if (layout_node.dom_node() && is<HTML::HTMLImageElement>(*layout_node.dom_node())) { if (layout_node.dom_node() && is<HTML::HTMLImageElement>(*layout_node.dom_node())) {
if (auto image_data = static_cast<HTML::HTMLImageElement const&>(*layout_node.dom_node()).current_request().image_data()) { if (auto image_data = static_cast<HTML::HTMLImageElement const&>(*layout_node.dom_node()).current_request().image_data()) {
if (is<SVG::SVGDecodedImageData>(*image_data)) { if (is<SVG::SVGDecodedImageData>(*image_data)) {

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -463,7 +464,7 @@ int HTMLElement::offset_top() const
if (!paintable_box()) if (!paintable_box())
return 0; return 0;
CSSPixels top_border_edge_of_element = paintable_box()->absolute_border_box_rect().y(); CSSPixels top_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().y();
// 2. If the offsetParent of the element is null // 2. If the offsetParent of the element is null
// return the y-coordinate of the top border edge of the first CSS layout box associated with the element, // return the y-coordinate of the top border edge of the first CSS layout box associated with the element,
@ -487,7 +488,7 @@ int HTMLElement::offset_top() const
if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
top_padding_edge_of_offset_parent = 0; top_padding_edge_of_offset_parent = 0;
} else { } else {
top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().y(); top_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().y();
} }
return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int(); return (top_border_edge_of_element - top_padding_edge_of_offset_parent).to_int();
} }
@ -505,7 +506,7 @@ int HTMLElement::offset_left() const
if (!paintable_box()) if (!paintable_box())
return 0; return 0;
CSSPixels left_border_edge_of_element = paintable_box()->absolute_border_box_rect().x(); CSSPixels left_border_edge_of_element = paintable_box()->absolute_united_border_box_rect().x();
// 2. If the offsetParent of the element is null // 2. If the offsetParent of the element is null
// return the x-coordinate of the left border edge of the first CSS layout box associated with the element, // return the x-coordinate of the left border edge of the first CSS layout box associated with the element,
@ -529,7 +530,7 @@ int HTMLElement::offset_left() const
if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) { if (offset_parent->is_html_body_element() && !offset_parent->paintable_box()->is_positioned()) {
left_padding_edge_of_offset_parent = 0; left_padding_edge_of_offset_parent = 0;
} else { } else {
left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_padding_box_rect().x(); left_padding_edge_of_offset_parent = offset_parent->paintable_box()->absolute_united_padding_box_rect().x();
} }
return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int(); return (left_border_edge_of_element - left_padding_edge_of_offset_parent).to_int();
} }
@ -540,13 +541,17 @@ int HTMLElement::offset_width() const
// NOTE: Ensure that layout is up-to-date before looking at metrics. // NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(); const_cast<DOM::Document&>(document()).update_layout();
// 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. // 1. If the element does not have any associated box return zero and terminate this algorithm.
if (!paintable_box()) auto const* box = paintable_box();
if (!box)
return 0; return 0;
// 2. Return the width of the axis-aligned bounding box of the border boxes of all fragments generated by the elements principal box, // 2. Return the unscaled width of the axis-aligned bounding box of the border boxes of all fragments generated by
// ignoring any transforms that apply to the element and its ancestors. // the elements principal box, ignoring any transforms that apply to the element and its ancestors.
return paintable_box()->border_box_width().to_int(); //
// If the elements principal box is an inline-level box which was "split" by a block-level descendant, also
// include fragments generated by the block-level descendants, unless they are zero width or height.
return box->absolute_united_border_box_rect().width().to_int();
} }
// https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetheight
@ -555,13 +560,17 @@ int HTMLElement::offset_height() const
// NOTE: Ensure that layout is up-to-date before looking at metrics. // NOTE: Ensure that layout is up-to-date before looking at metrics.
const_cast<DOM::Document&>(document()).update_layout(); const_cast<DOM::Document&>(document()).update_layout();
// 1. If the element does not have any associated CSS layout box return zero and terminate this algorithm. // 1. If the element does not have any associated box return zero and terminate this algorithm.
if (!paintable_box()) auto const* box = paintable_box();
if (!box)
return 0; return 0;
// 2. Return the height of the axis-aligned bounding box of the border boxes of all fragments generated by the elements principal box, // 2. Return the unscaled height of the axis-aligned bounding box of the border boxes of all fragments generated by
// ignoring any transforms that apply to the element and its ancestors. // the elements principal box, ignoring any transforms that apply to the element and its ancestors.
return paintable_box()->border_box_height().to_int(); //
// If the elements principal box is an inline-level box which was "split" by a block-level descendant, also
// include fragments generated by the block-level descendants, unless they are zero width or height.
return box->absolute_united_border_box_rect().height().to_int();
} }
// https://html.spec.whatwg.org/multipage/links.html#cannot-navigate // https://html.spec.whatwg.org/multipage/links.html#cannot-navigate

View file

@ -7,7 +7,6 @@
#pragma once #pragma once
#include <AK/OwnPtr.h> #include <AK/OwnPtr.h>
#include <LibGfx/Rect.h>
#include <LibJS/Heap/Cell.h> #include <LibJS/Heap/Cell.h>
#include <LibWeb/Layout/Node.h> #include <LibWeb/Layout/Node.h>

View file

@ -8,7 +8,6 @@
#include <LibWeb/DOM/Document.h> #include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h> #include <LibWeb/DOM/Element.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/InlineFormattingContext.h> #include <LibWeb/Layout/InlineFormattingContext.h>
#include <LibWeb/Layout/InlineNode.h> #include <LibWeb/Layout/InlineNode.h>

View file

@ -222,7 +222,12 @@ void LayoutState::commit(Box& root)
root.document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) { root.document().for_each_shadow_including_inclusive_descendant([&](DOM::Node& node) {
node.clear_paintable(); node.clear_paintable();
if (node.layout_node() && is<InlineNode>(node.layout_node())) { if (node.layout_node() && is<InlineNode>(node.layout_node())) {
inline_nodes.set(static_cast<InlineNode*>(node.layout_node())); // Inline nodes might have a continuation chain; add all inline nodes that are part of it.
for (GC::Ptr inline_node = static_cast<NodeWithStyleAndBoxModelMetrics*>(node.layout_node());
inline_node; inline_node = inline_node->continuation_of_node()) {
if (is<InlineNode>(*inline_node))
inline_nodes.set(static_cast<InlineNode*>(inline_node.ptr()));
}
} }
return TraversalDecision::Continue; return TraversalDecision::Continue;
}); });

View file

@ -1,6 +1,7 @@
/* /*
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org> * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -13,7 +14,6 @@
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h> #include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h> #include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h> #include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/MathDepthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h> #include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h> #include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h> #include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
@ -30,7 +30,6 @@
#include <LibWeb/Layout/TableWrapper.h> #include <LibWeb/Layout/TableWrapper.h>
#include <LibWeb/Layout/TextNode.h> #include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h> #include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Platform/FontPlugin.h>
namespace Web::Layout { namespace Web::Layout {
@ -328,7 +327,7 @@ static CSSPixels snap_a_length_as_a_border_width(double device_pixels_per_css_pi
return length; return length;
} }
void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style) void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
{ {
auto& computed_values = mutable_computed_values(); auto& computed_values = mutable_computed_values();
@ -1015,6 +1014,9 @@ void NodeWithStyle::apply_style(const CSS::ComputedProperties& computed_style)
computed_values.set_isolation(isolation.value()); computed_values.set_isolation(isolation.value());
propagate_style_to_anonymous_wrappers(); propagate_style_to_anonymous_wrappers();
if (is<NodeWithStyleAndBoxModelMetrics>(this))
static_cast<NodeWithStyleAndBoxModelMetrics&>(*this).propagate_style_along_continuation(computed_style);
} }
void NodeWithStyle::propagate_style_to_anonymous_wrappers() void NodeWithStyle::propagate_style_to_anonymous_wrappers()
@ -1278,4 +1280,18 @@ CSS::UserSelect Node::user_select_used_value() const
return computed_value; return computed_value;
} }
void NodeWithStyleAndBoxModelMetrics::propagate_style_along_continuation(CSS::ComputedProperties const& computed_style) const
{
for (auto continuation = continuation_of_node(); continuation; continuation = continuation->continuation_of_node()) {
if (!continuation->is_anonymous())
continuation->apply_style(computed_style);
}
}
void NodeWithStyleAndBoxModelMetrics::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_continuation_of_node);
}
} }

View file

@ -230,7 +230,7 @@ public:
CSS::ImmutableComputedValues const& computed_values() const { return static_cast<CSS::ImmutableComputedValues const&>(*m_computed_values); } CSS::ImmutableComputedValues const& computed_values() const { return static_cast<CSS::ImmutableComputedValues const&>(*m_computed_values); }
CSS::MutableComputedValues& mutable_computed_values() { return static_cast<CSS::MutableComputedValues&>(*m_computed_values); } CSS::MutableComputedValues& mutable_computed_values() { return static_cast<CSS::MutableComputedValues&>(*m_computed_values); }
void apply_style(const CSS::ComputedProperties&); void apply_style(CSS::ComputedProperties const&);
Gfx::Font const& first_available_font() const; Gfx::Font const& first_available_font() const;
Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); } Vector<CSS::BackgroundLayerData> const& background_layers() const { return computed_values().background_layers(); }
@ -266,6 +266,13 @@ public:
BoxModelMetrics& box_model() { return m_box_model; } BoxModelMetrics& box_model() { return m_box_model; }
BoxModelMetrics const& box_model() const { return m_box_model; } BoxModelMetrics const& box_model() const { return m_box_model; }
GC::Ptr<NodeWithStyleAndBoxModelMetrics> continuation_of_node() const { return m_continuation_of_node; }
void set_continuation_of_node(Badge<TreeBuilder>, GC::Ptr<NodeWithStyleAndBoxModelMetrics> node) { m_continuation_of_node = node; }
void propagate_style_along_continuation(CSS::ComputedProperties const&) const;
virtual void visit_edges(Cell::Visitor& visitor) override;
protected: protected:
NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, GC::Ref<CSS::ComputedProperties> style) NodeWithStyleAndBoxModelMetrics(DOM::Document& document, DOM::Node* node, GC::Ref<CSS::ComputedProperties> style)
: NodeWithStyle(document, node, style) : NodeWithStyle(document, node, style)
@ -281,6 +288,7 @@ private:
virtual bool is_node_with_style_and_box_model_metrics() const final { return true; } virtual bool is_node_with_style_and_box_model_metrics() const final { return true; }
BoxModelMetrics m_box_model; BoxModelMetrics m_box_model;
GC::Ptr<NodeWithStyleAndBoxModelMetrics> m_continuation_of_node;
}; };
template<> template<>

View file

@ -2,15 +2,14 @@
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org> * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, MacDue <macdue@dueutil.tech> * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <AK/Error.h>
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/TemporaryChange.h> #include <AK/TemporaryChange.h>
#include <LibWeb/CSS/StyleComputer.h> #include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h> #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h> #include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/DOM/Document.h> #include <LibWeb/DOM/Document.h>
@ -18,7 +17,6 @@
#include <LibWeb/DOM/ParentNode.h> #include <LibWeb/DOM/ParentNode.h>
#include <LibWeb/DOM/ShadowRoot.h> #include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/Dump.h> #include <LibWeb/Dump.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
#include <LibWeb/HTML/HTMLInputElement.h> #include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLLIElement.h> #include <LibWeb/HTML/HTMLLIElement.h>
#include <LibWeb/HTML/HTMLOListElement.h> #include <LibWeb/HTML/HTMLOListElement.h>
@ -95,6 +93,10 @@ static Layout::Node& insertion_parent_for_inline_node(Layout::NodeWithStyle& lay
static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layout_parent, Layout::Node& layout_node) static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layout_parent, Layout::Node& layout_node)
{ {
// Inline is fine for in-flow block children; we'll maintain the (non-)inline invariant after insertion.
if (layout_parent.is_inline() && layout_parent.display().is_flow_inside() && !layout_node.is_out_of_flow())
return layout_parent;
if (!has_inline_or_in_flow_block_children(layout_parent)) { if (!has_inline_or_in_flow_block_children(layout_parent)) {
// Parent block has no children, insert this block into parent. // Parent block has no children, insert this block into parent.
return layout_parent; return layout_parent;
@ -121,26 +123,25 @@ static Layout::Node& insertion_parent_for_block_node(Layout::NodeWithStyle& layo
return layout_parent; return layout_parent;
} }
// Parent block has inline-level children (our siblings). // Parent block has inline-level children (our siblings); wrap these siblings into an anonymous wrapper block.
// First move these siblings into an anonymous wrapper block. Vector<GC::Ref<Node>> children;
Vector<GC::Root<Layout::Node>> children; for (GC::Ptr<Node> child = layout_parent.first_child(); child; child = child->next_sibling()) {
{ // NOTE: We let out-of-flow children stay in the parent, to preserve tree structure.
GC::Ptr<Layout::Node> next; if (child->is_out_of_flow())
for (GC::Ptr<Layout::Node> child = layout_parent.first_child(); child; child = next) { continue;
next = child->next_sibling(); children.append(*child);
// NOTE: We let out-of-flow children stay in the parent, to preserve tree structure.
if (child->is_out_of_flow())
continue;
layout_parent.remove_child(*child);
children.append(*child);
}
} }
layout_parent.append_child(layout_parent.create_anonymous_wrapper());
auto wrapper = layout_parent.create_anonymous_wrapper();
wrapper->set_children_are_inline(true);
for (auto child : children) {
layout_parent.remove_child(child);
wrapper->append_child(child);
}
layout_parent.set_children_are_inline(false); layout_parent.set_children_are_inline(false);
for (auto& child : children) { layout_parent.append_child(wrapper);
layout_parent.last_child()->append_child(*child);
}
layout_parent.last_child()->set_children_are_inline(true);
// Then it's safe to insert this block into parent. // Then it's safe to insert this block into parent.
return layout_parent; return layout_parent;
} }
@ -150,45 +151,35 @@ void TreeBuilder::insert_node_into_inline_or_block_ancestor(Layout::Node& node,
if (node.display().is_contents()) if (node.display().is_contents())
return; return;
if (display.is_inline_outside()) { // Find the nearest ancestor that can host the node.
// Inlines can be inserted into the nearest ancestor without "display: contents". auto& nearest_insertion_ancestor = [&]() -> NodeWithStyle& {
auto& nearest_ancestor_without_display_contents = [&]() -> Layout::NodeWithStyle& { for (auto& ancestor : m_ancestor_stack.in_reverse()) {
for (auto& ancestor : m_ancestor_stack.in_reverse()) { auto const& ancestor_display = ancestor->display();
if (!ancestor->display().is_contents())
return ancestor;
}
VERIFY_NOT_REACHED();
}();
auto& insertion_point = insertion_parent_for_inline_node(nearest_ancestor_without_display_contents);
if (mode == AppendOrPrepend::Prepend)
insertion_point.prepend_child(node);
else
insertion_point.append_child(node);
insertion_point.set_children_are_inline(true);
} else {
// Non-inlines can't be inserted into an inline parent, so find the nearest non-inline ancestor.
auto& nearest_non_inline_ancestor = [&]() -> Layout::NodeWithStyle& {
for (auto& ancestor : m_ancestor_stack.in_reverse()) {
if (ancestor->display().is_contents())
continue;
if (!ancestor->display().is_inline_outside())
return ancestor;
if (!ancestor->display().is_flow_inside())
return ancestor;
if (ancestor->dom_node() && is<SVG::SVGForeignObjectElement>(*ancestor->dom_node()))
return ancestor;
}
VERIFY_NOT_REACHED();
}();
auto& insertion_point = insertion_parent_for_block_node(nearest_non_inline_ancestor, node);
if (mode == AppendOrPrepend::Prepend)
insertion_point.prepend_child(node);
else
insertion_point.append_child(node);
// Out-of-flow nodes cannot be hosted in inline flow nodes.
if (node.is_out_of_flow() && ancestor_display.is_inline_outside() && ancestor_display.is_flow_inside())
continue;
if (!ancestor_display.is_contents())
return ancestor;
}
VERIFY_NOT_REACHED();
}();
auto& insertion_point = display.is_inline_outside() ? insertion_parent_for_inline_node(nearest_insertion_ancestor)
: insertion_parent_for_block_node(nearest_insertion_ancestor, node);
if (mode == AppendOrPrepend::Prepend)
insertion_point.prepend_child(node);
else
insertion_point.append_child(node);
if (display.is_inline_outside()) {
// After inserting an inline-level box into a parent, mark the parent as having inline children.
insertion_point.set_children_are_inline(true);
} else if (node.is_in_flow()) {
// After inserting an in-flow block-level box into a parent, mark the parent as having non-inline children. // After inserting an in-flow block-level box into a parent, mark the parent as having non-inline children.
if (!node.is_floating() && !node.is_absolutely_positioned()) insertion_point.set_children_are_inline(false);
insertion_point.set_children_are_inline(false);
} }
} }
@ -261,6 +252,159 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Se
pseudo_element_node->mutable_computed_values().set_content(pseudo_element_content); pseudo_element_node->mutable_computed_values().set_content(pseudo_element_content);
} }
// Block nodes inside inline nodes are allowed, but to maintain the invariant that either all layout children are
// inline or non-inline, we need to rearrange the tree a bit. All inline ancestors up to the node we've inserted are
// wrapped in an anonymous block, which is inserted into the nearest non-inline ancestor. We then recreate the inline
// ancestors in another anonymous block inserted after the node so we can continue adding children.
//
// Effectively, we try to turn this:
//
// InlineNode 1
// TextNode 1
// InlineNode N
// TextNode N
// BlockContainer (node)
//
// Into this:
//
// BlockContainer (anonymous "before")
// InlineNode 1
// TextNode 1
// InlineNode N
// TextNode N
// BlockContainer (anonymous "middle") continuation
// BlockContainer (node)
// BlockContainer (anonymous "after")
// InlineNode 1 continuation
// InlineNode N
//
// To be able to reconstruct their relation after restructuring, layout nodes keep track of their continuation. The
// top-most inline node of the "after" wrapper points to the "middle" wrapper, which points to the top-most inline node
// of the "before" wrapper. All other inline nodes in the "after" wrapper point to their counterparts in the "before"
// wrapper, to make it easier to create the right paintables since a DOM::Node only has a single Layout::Node.
//
// Appending then continues in the "after" tree. If a new block node is then inserted, we can reuse the "middle" wrapper
// if no inline siblings exist for node or its ancestors, and leave the existing "after" wrapper alone. Otherwise, we
// create new wrappers and extend the continuation chain.
//
// Inspired by: https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/
void TreeBuilder::restructure_block_node_in_inline_parent(NodeWithStyleAndBoxModelMetrics& node)
{
// Mark parent as inline again
auto& parent = *node.parent();
VERIFY(!parent.children_are_inline());
parent.set_children_are_inline(true);
// Find nearest non-inline, content supporting ancestor that is not an anonymous block.
auto& nearest_block_ancestor = [&] -> NodeWithStyle& {
for (auto* ancestor = parent.parent(); ancestor; ancestor = ancestor->parent()) {
if (!ancestor->is_inline() && !ancestor->display().is_contents() && !ancestor->is_anonymous())
return *ancestor;
}
VERIFY_NOT_REACHED();
}();
nearest_block_ancestor.set_children_are_inline(false);
// Unwind the ancestor stack to find the topmost inline ancestor.
GC::Ptr<NodeWithStyleAndBoxModelMetrics> topmost_inline_ancestor;
for (auto* ancestor = &parent; ancestor; ancestor = ancestor->parent()) {
if (ancestor == &nearest_block_ancestor)
break;
if (ancestor == m_ancestor_stack.last())
m_ancestor_stack.take_last();
if (ancestor->is_inline())
topmost_inline_ancestor = static_cast<NodeWithStyleAndBoxModelMetrics*>(ancestor);
}
VERIFY(topmost_inline_ancestor);
// We need to host the topmost inline ancestor and its previous siblings in an anonymous "before" wrapper. If an
// inline wrapper does not already exist, we create a new one and add it to the nearest block ancestor.
GC::Ptr<Node> before_wrapper;
if (auto last_child = nearest_block_ancestor.last_child(); last_child->is_anonymous() && last_child->children_are_inline()) {
before_wrapper = last_child;
} else {
before_wrapper = nearest_block_ancestor.create_anonymous_wrapper();
before_wrapper->set_children_are_inline(true);
nearest_block_ancestor.append_child(*before_wrapper);
}
if (topmost_inline_ancestor->parent() != before_wrapper.ptr()) {
GC::Ptr<Node> inline_to_move = topmost_inline_ancestor;
while (inline_to_move) {
auto* next = inline_to_move->previous_sibling();
inline_to_move->remove();
before_wrapper->insert_before(*inline_to_move, before_wrapper->first_child());
inline_to_move = next;
}
}
// If we are part of an existing continuation and all inclusive ancestors have no previous siblings, we can reuse
// the existing middle wrapper. Otherwiser, we create a new middle wrapper to contain the block node and add it to
// the nearest block ancestor.
bool needs_new_continuation = true;
GC::Ptr<NodeWithStyleAndBoxModelMetrics> middle_wrapper;
if (topmost_inline_ancestor->continuation_of_node()) {
needs_new_continuation = false;
for (GC::Ptr<Node> ancestor = node; ancestor != topmost_inline_ancestor; ancestor = ancestor->parent()) {
if (ancestor->previous_sibling()) {
needs_new_continuation = true;
break;
}
}
if (!needs_new_continuation)
middle_wrapper = topmost_inline_ancestor->continuation_of_node();
}
if (!middle_wrapper) {
middle_wrapper = static_cast<NodeWithStyleAndBoxModelMetrics&>(*nearest_block_ancestor.create_anonymous_wrapper());
nearest_block_ancestor.append_child(*middle_wrapper);
middle_wrapper->set_continuation_of_node({}, topmost_inline_ancestor);
}
// Move the block node to the middle wrapper.
node.remove();
middle_wrapper->append_child(node);
// If we need a new continuation, recreate inline ancestors in another anonymous block so we can continue adding new
// nodes. We don't need to do this if we are within an existing continuation and there were no previous siblings in
// any inclusive ancestor of node in the after wrapper.
if (needs_new_continuation) {
auto after_wrapper = nearest_block_ancestor.create_anonymous_wrapper();
GC::Ptr<Node> current_parent = after_wrapper;
for (GC::Ptr<Node> inline_node = topmost_inline_ancestor;
inline_node && is<DOM::Element>(inline_node->dom_node()); inline_node = inline_node->last_child()) {
auto& element = static_cast<DOM::Element&>(*inline_node->dom_node());
auto style = element.computed_properties();
auto& new_inline_node = static_cast<NodeWithStyleAndBoxModelMetrics&>(*element.create_layout_node(*style));
if (inline_node == topmost_inline_ancestor) {
// The topmost inline ancestor points to the middle wrapper, which in turns points to the original node.
new_inline_node.set_continuation_of_node({}, middle_wrapper);
topmost_inline_ancestor = new_inline_node;
} else {
// We need all other inline nodes to point to their original node so we can walk the continuation chain
// in LayoutState and create the right paintables.
new_inline_node.set_continuation_of_node({}, static_cast<NodeWithStyleAndBoxModelMetrics&>(*inline_node));
}
current_parent->append_child(new_inline_node);
current_parent = new_inline_node;
// Stop recreating nodes when we've reached node's parent
if (inline_node == &parent)
break;
}
after_wrapper->set_children_are_inline(true);
nearest_block_ancestor.append_child(after_wrapper);
}
// Rewind the ancestor stack
for (GC::Ptr<Node> inline_node = topmost_inline_ancestor; inline_node; inline_node = inline_node->last_child()) {
if (!is<NodeWithStyle>(*inline_node))
break;
m_ancestor_stack.append(static_cast<NodeWithStyle&>(*inline_node));
}
}
static bool is_ignorable_whitespace(Layout::Node const& node) static bool is_ignorable_whitespace(Layout::Node const& node)
{ {
if (node.is_text_node() && static_cast<TextNode const&>(node).text_for_rendering().bytes_as_string_view().is_whitespace()) if (node.is_text_node() && static_cast<TextNode const&>(node).text_for_rendering().bytes_as_string_view().is_whitespace())
@ -591,6 +735,14 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::After, AppendOrPrepend::Append); create_pseudo_element_if_needed(element, CSS::Selector::PseudoElement::Type::After, AppendOrPrepend::Append);
pop_parent(); pop_parent();
} }
// If we completely finished inserting a block level element into an inline parent, we need to fix up the tree so
// that we can maintain the invariant that all children are either inline or non-inline. We can't do this earlier,
// because the restructuring adds new children after this node that become part of the ancestor stack.
auto* layout_parent = layout_node->parent();
if (layout_parent && layout_parent->display().is_inline_outside() && !display.is_contents()
&& !display.is_inline_outside() && layout_parent->display().is_flow_inside() && !layout_node->is_out_of_flow())
restructure_block_node_in_inline_parent(static_cast<NodeWithStyleAndBoxModelMetrics&>(*layout_node));
} }
GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node) GC::Ptr<Layout::Node> TreeBuilder::build(DOM::Node& dom_node)

View file

@ -58,6 +58,7 @@ private:
}; };
void insert_node_into_inline_or_block_ancestor(Layout::Node&, CSS::Display, AppendOrPrepend); void insert_node_into_inline_or_block_ancestor(Layout::Node&, CSS::Display, AppendOrPrepend);
void create_pseudo_element_if_needed(DOM::Element&, CSS::Selector::PseudoElement::Type, AppendOrPrepend); void create_pseudo_element_if_needed(DOM::Element&, CSS::Selector::PseudoElement::Type, AppendOrPrepend);
void restructure_block_node_in_inline_parent(NodeWithStyleAndBoxModelMetrics&);
GC::Ptr<Layout::Node> m_layout_root; GC::Ptr<Layout::Node> m_layout_root;
Vector<GC::Ref<Layout::NodeWithStyle>> m_ancestor_stack; Vector<GC::Ref<Layout::NodeWithStyle>> m_ancestor_stack;

View file

@ -2,6 +2,7 @@
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org> * Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org> * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com> * Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -17,7 +18,6 @@
#include <LibWeb/HTML/Window.h> #include <LibWeb/HTML/Window.h>
#include <LibWeb/Layout/BlockContainer.h> #include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/InlineNode.h> #include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Painting/BackgroundPainting.h> #include <LibWeb/Painting/BackgroundPainting.h>
#include <LibWeb/Painting/PaintableBox.h> #include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/SVGPaintable.h> #include <LibWeb/Painting/SVGPaintable.h>
@ -214,6 +214,34 @@ CSSPixelRect PaintableBox::compute_absolute_paint_rect() const
return rect; return rect;
} }
CSSPixelRect PaintableBox::absolute_padding_box_rect() const
{
auto absolute_rect = this->absolute_rect();
CSSPixelRect rect;
rect.set_x(absolute_rect.x() - box_model().padding.left);
rect.set_width(content_width() + box_model().padding.left + box_model().padding.right);
rect.set_y(absolute_rect.y() - box_model().padding.top);
rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom);
return rect;
}
CSSPixelRect PaintableBox::absolute_border_box_rect() const
{
auto padded_rect = this->absolute_padding_box_rect();
CSSPixelRect rect;
auto use_collapsing_borders_model = override_borders_data().has_value();
// Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders.
auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top;
auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom;
auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left;
auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right;
rect.set_x(padded_rect.x() - border_left);
rect.set_width(padded_rect.width() + border_left + border_right);
rect.set_y(padded_rect.y() - border_top);
rect.set_height(padded_rect.height() + border_top + border_bottom);
return rect;
}
CSSPixelRect PaintableBox::absolute_paint_rect() const CSSPixelRect PaintableBox::absolute_paint_rect() const
{ {
if (!m_absolute_paint_rect.has_value()) if (!m_absolute_paint_rect.has_value())
@ -221,6 +249,51 @@ CSSPixelRect PaintableBox::absolute_paint_rect() const
return *m_absolute_paint_rect; return *m_absolute_paint_rect;
} }
template<typename Callable>
static CSSPixelRect united_rect_for_continuation_chain(PaintableBox const& start, Callable get_rect)
{
// Combine the absolute rects of all paintable boxes of all nodes in the continuation chain. Without this, we
// calculate the wrong rect for inline nodes that were split because of block elements.
Optional<CSSPixelRect> result;
// FIXME: instead of walking the continuation chain in the layout tree, also keep track of this chain in the
// painting tree so we can skip visiting the layout nodes altogether.
for (auto const* node = &start.layout_node_with_style_and_box_metrics(); node; node = node->continuation_of_node()) {
for (auto const& paintable : node->paintables()) {
if (!is<PaintableBox>(paintable))
continue;
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
auto paintable_border_box_rect = get_rect(paintable_box);
if (!result.has_value())
result = paintable_border_box_rect;
else if (!paintable_border_box_rect.is_empty())
result->unite(paintable_border_box_rect);
}
}
return result.value_or({});
}
CSSPixelRect PaintableBox::absolute_united_border_box_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_border_box_rect();
});
}
CSSPixelRect PaintableBox::absolute_united_content_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_rect();
});
}
CSSPixelRect PaintableBox::absolute_united_padding_box_rect() const
{
return united_rect_for_continuation_chain(*this, [](auto const& paintable_box) {
return paintable_box.absolute_padding_box_rect();
});
}
Optional<CSSPixelRect> PaintableBox::get_clip_rect() const Optional<CSSPixelRect> PaintableBox::get_clip_rect() const
{ {
auto clip = computed_values().clip(); auto clip = computed_values().clip();
@ -396,17 +469,18 @@ void PaintableBox::paint(PaintContext& context, PaintPhase phase) const
} }
if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node_with_style_and_box_metrics()) { if (phase == PaintPhase::Overlay && layout_node().document().inspected_layout_node() == &layout_node_with_style_and_box_metrics()) {
auto content_rect = absolute_rect(); auto content_rect = absolute_united_content_rect();
auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) {
auto margin_box = box_model().margin_box(); auto margin_box = box.box_model().margin_box();
CSSPixelRect margin_rect; return CSSPixelRect {
margin_rect.set_x(absolute_x() - margin_box.left); box.absolute_x() - margin_box.left,
margin_rect.set_width(content_width() + margin_box.left + margin_box.right); box.absolute_y() - margin_box.top,
margin_rect.set_y(absolute_y() - margin_box.top); box.content_width() + margin_box.left + margin_box.right,
margin_rect.set_height(content_height() + margin_box.top + margin_box.bottom); box.content_height() + margin_box.top + margin_box.bottom,
};
auto border_rect = absolute_border_box_rect(); });
auto padding_rect = absolute_padding_box_rect(); auto border_rect = absolute_united_border_box_rect();
auto padding_rect = absolute_united_padding_box_rect();
auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) { auto paint_inspector_rect = [&](CSSPixelRect const& rect, Color color) {
auto device_rect = context.enclosing_device_rect(rect).to_type<int>(); auto device_rect = context.enclosing_device_rect(rect).to_type<int>();
@ -895,7 +969,7 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ
if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y()))
return TraversalDecision::Continue; return TraversalDecision::Continue;
if (!visible_for_hit_testing()) if (!absolute_border_box_rect().contains(position_adjusted_by_scroll_offset))
return TraversalDecision::Continue; return TraversalDecision::Continue;
return callback(HitTestResult { const_cast<PaintableBox&>(*this) }); return callback(HitTestResult { const_cast<PaintableBox&>(*this) });

View file

@ -57,8 +57,6 @@ public:
CSSPixelPoint scroll_offset {}; CSSPixelPoint scroll_offset {};
}; };
CSSPixelRect absolute_rect() const;
// Offset from the top left of the containing block's content edge. // Offset from the top left of the containing block's content edge.
[[nodiscard]] CSSPixelPoint offset() const; [[nodiscard]] CSSPixelPoint offset() const;
@ -78,36 +76,16 @@ public:
CSSPixels content_width() const { return m_content_size.width(); } CSSPixels content_width() const { return m_content_size.width(); }
CSSPixels content_height() const { return m_content_size.height(); } CSSPixels content_height() const { return m_content_size.height(); }
CSSPixelRect absolute_padding_box_rect() const CSSPixelRect absolute_rect() const;
{ CSSPixelRect absolute_padding_box_rect() const;
auto absolute_rect = this->absolute_rect(); CSSPixelRect absolute_border_box_rect() const;
CSSPixelRect rect;
rect.set_x(absolute_rect.x() - box_model().padding.left);
rect.set_width(content_width() + box_model().padding.left + box_model().padding.right);
rect.set_y(absolute_rect.y() - box_model().padding.top);
rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom);
return rect;
}
CSSPixelRect absolute_border_box_rect() const
{
auto padded_rect = this->absolute_padding_box_rect();
CSSPixelRect rect;
auto use_collapsing_borders_model = override_borders_data().has_value();
// Implement the collapsing border model https://www.w3.org/TR/CSS22/tables.html#collapsing-borders.
auto border_top = use_collapsing_borders_model ? round(box_model().border.top / 2) : box_model().border.top;
auto border_bottom = use_collapsing_borders_model ? round(box_model().border.bottom / 2) : box_model().border.bottom;
auto border_left = use_collapsing_borders_model ? round(box_model().border.left / 2) : box_model().border.left;
auto border_right = use_collapsing_borders_model ? round(box_model().border.right / 2) : box_model().border.right;
rect.set_x(padded_rect.x() - border_left);
rect.set_width(padded_rect.width() + border_left + border_right);
rect.set_y(padded_rect.y() - border_top);
rect.set_height(padded_rect.height() + border_top + border_bottom);
return rect;
}
CSSPixelRect absolute_paint_rect() const; CSSPixelRect absolute_paint_rect() const;
// These united versions of the above rects take continuation into account.
CSSPixelRect absolute_united_border_box_rect() const;
CSSPixelRect absolute_united_content_rect() const;
CSSPixelRect absolute_united_padding_box_rect() const;
CSSPixels border_box_width() const CSSPixels border_box_width() const
{ {
auto border_box = box_model().border_box(); auto border_box = box_model().border_box();

View file

@ -33,23 +33,28 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
TextNode <#text> TextNode <#text>
InlineNode <form> InlineNode <form>
TextNode <#text> TextNode <#text>
BlockContainer <(anonymous)> at (235,65) content-size 139.96875x19 children: not-inline continuation
BlockContainer <p> at (235,65) content-size 139.96875x19 children: inline
frag 0 from TextNode start: 1, length: 5, rect: [235,65 27.5x19] baseline: 12.5
"bang "
frag 1 from RadioButton start: 0, length: 0, rect: [262.5,65 12x12] baseline: 12
TextNode <#text> TextNode <#text>
RadioButton <input> at (262.5,65) content-size 12x12 inline-block children: not-inline
TextNode <#text>
BlockContainer <(anonymous)> at (235,84) content-size 139.96875x0 children: inline
InlineNode <form> continuation
TextNode <#text>
BlockContainer <(anonymous)> at (235,84) content-size 139.96875x19 children: not-inline continuation
BlockContainer <p> at (235,84) content-size 139.96875x19 children: inline
frag 0 from TextNode start: 1, length: 8, rect: [235,84 45.171875x19] baseline: 12.5
"whimper "
frag 1 from RadioButton start: 0, length: 0, rect: [280.171875,84 12x12] baseline: 12
TextNode <#text>
RadioButton <input> at (280.171875,84) content-size 12x12 inline-block children: not-inline
TextNode <#text> TextNode <#text>
BlockContainer <p> at (235,65) content-size 139.96875x19 children: inline
frag 0 from TextNode start: 1, length: 5, rect: [235,65 27.5x19] baseline: 12.5
"bang "
frag 1 from RadioButton start: 0, length: 0, rect: [262.5,65 12x12] baseline: 12
TextNode <#text>
RadioButton <input> at (262.5,65) content-size 12x12 inline-block children: not-inline
TextNode <#text>
BlockContainer <p> at (235,84) content-size 139.96875x19 children: inline
frag 0 from TextNode start: 1, length: 8, rect: [235,84 45.171875x19] baseline: 12.5
"whimper "
frag 1 from RadioButton start: 0, length: 0, rect: [280.171875,84 12x12] baseline: 12
TextNode <#text>
RadioButton <input> at (280.171875,84) content-size 12x12 inline-block children: not-inline
TextNode <#text>
BlockContainer <(anonymous)> at (235,103) content-size 139.96875x0 children: inline BlockContainer <(anonymous)> at (235,103) content-size 139.96875x0 children: inline
InlineNode <form> continuation
TextNode <#text>
TextNode <#text> TextNode <#text>
TextNode <#text> TextNode <#text>
BlockContainer <li> at (409.96875,60) content-size 50x90 floating [BFC] children: inline BlockContainer <li> at (409.96875,60) content-size 50x90 floating [BFC] children: inline
@ -136,13 +141,18 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600]
TextPaintable (TextNode<#text>) TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x0] PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x0]
PaintableWithLines (InlineNode<FORM>) PaintableWithLines (InlineNode<FORM>)
PaintableWithLines (BlockContainer<P>) [235,65 139.96875x19] PaintableWithLines (BlockContainer(anonymous)) [235,65 139.96875x19]
TextPaintable (TextNode<#text>) PaintableWithLines (BlockContainer<P>) [235,65 139.96875x19]
RadioButtonPaintable (RadioButton<INPUT>) [262.5,65 12x12] TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<P>) [235,84 139.96875x19] RadioButtonPaintable (RadioButton<INPUT>) [262.5,65 12x12]
TextPaintable (TextNode<#text>) PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x0]
RadioButtonPaintable (RadioButton<INPUT>) [280.171875,84 12x12] PaintableWithLines (InlineNode<FORM>)
PaintableWithLines (BlockContainer(anonymous)) [235,84 139.96875x19]
PaintableWithLines (BlockContainer<P>) [235,84 139.96875x19]
TextPaintable (TextNode<#text>)
RadioButtonPaintable (RadioButton<INPUT>) [280.171875,84 12x12]
PaintableWithLines (BlockContainer(anonymous)) [235,103 139.96875x0] PaintableWithLines (BlockContainer(anonymous)) [235,103 139.96875x0]
PaintableWithLines (InlineNode<FORM>)
PaintableWithLines (BlockContainer<LI>) [394.96875,45 80x120] PaintableWithLines (BlockContainer<LI>) [394.96875,45 80x120]
TextPaintable (TextNode<#text>) TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<LI>#baz) [135,175 120x120] PaintableWithLines (BlockContainer<LI>#baz) [135,175 120x120]

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<body>
<b>foo</b><div><b>bar</b></div>
<hr>
<div><b>foo</b></div><b>bar</b>
<hr>
<b>foo</b><div><b>bar</b></div><b>baz</b>
<hr>
<b>foo<i>bar</i></b><div><b><i>baz</i></b></div><b><i>lorem</i>ipsum</b>
<hr>
<b>foo</b><div><b>bar</b></div><div><b>baz</b></div><b>lorem</b>
<hr>
<b>foo</b><div><b>bar</b></div><b><u>baz</u></b><div><b>lorem</b></div><b>ipsum</b>
<hr>
<div>foo<div><b>bar</b></div></div>
<hr>
<b>foo</b><div><b>bar</b></div><b>baz</b>
<hr>
<span>foo</span><div>bar</div>
</body>
</html>

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<link rel="match" href="../expected/block-element-inside-inline-element-ref.html" />
</head>
<body>
<!-- Block at end of inline element -->
<b>foo<div>bar</div></b>
<!-- Block at beginning of inline element -->
<hr>
<b><div>foo</div>bar</b>
<!-- Block in middle of inline element -->
<hr>
<b>foo<div>bar</div>baz</b>
<!-- Block in middle of two inline elements -->
<hr>
<b>foo<i>bar<div>baz</div>lorem</i>ipsum</b>
<!-- Multiple subsequent blocks -->
<hr>
<b>foo<div>bar</div><div>baz</div>lorem</b>
<!-- Multiple subsequent blocks with inline element between them -->
<hr>
<b>foo<div>bar</div><u>baz</u><div>lorem</div>ipsum</b>
<!-- Block in inline element following inline element -->
<hr>
<div>foo<b><div>bar</div></b></div>
<!-- Dynamic tree mutation test -->
<hr>
<div id="target1"></div>
<script>
document.querySelector('#target1').innerHTML = '<b>foo<div>bar</div>baz</b>';
</script>
<!-- Dynamic style update -->
<hr>
<span id="target2" style="font-weight: bold">foo <div>bar</div></span>
<script>
window.addEventListener('DOMContentLoaded', () => {
document.body.offsetWidth; // force layout
const target2 = document.querySelector('#target2');
target2.setAttribute('style', null);
});
</script>
</body>
</html>

View file

@ -0,0 +1,4 @@
b.offsetTop: 8
b.offsetLeft: 8
b.offsetWidth: 784
b.offsetHeight: 51

View file

@ -0,0 +1,16 @@
<#text >
index: 1
<BODY >
---
<#text >
index: 1
<B id="b1" >
---
<#text >
index: 1
<DIV id="d1" >
---
<#text >
index: 3
<B id="b1" >
---

View file

@ -0,0 +1,12 @@
<b>foo<div>bar</div>baz</b>
<script src="../include.js"></script>
<script>
test(() => {
const b = document.querySelector('b');
println(`b.offsetTop: ${b.offsetTop}`);
println(`b.offsetLeft: ${b.offsetLeft}`);
println(`b.offsetWidth: ${b.offsetWidth}`);
println(`b.offsetHeight: ${b.offsetHeight}`);
});
</script>

View file

@ -0,0 +1,23 @@
<style>
* {
font-family: sans-serif;
}
</style>
<script src="../include.js"></script>
<body>foo<b id="b1">bar<div id="d1">baz</div>lorem</b>
<script>
test(() => {
logHitTest = (x, y) => {
const hit = internals.hitTest(x, y);
printElement(hit.node);
println(`index: ${hit.indexInNode}`);
printElement(hit.node.parentNode);
println('---');
}
logHitTest(20, 15);
logHitTest(48, 15);
logHitTest(23, 30);
logHitTest(33, 50);
});
</script>
</body>