1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-21 08:05:08 +00:00
serenity/Userland/Libraries/LibWeb/Dump.cpp
Andreas Kling 7dae895704 LibWeb: Align actual baselines, not just bottoms
Until now, we've been treating the bottom of every line box fragment as
its baseline, and just aligning all the bottoms to the bottom of the
line box. That gave decent results in many cases, but was not correct.

This patch starts moving towards actual baseline calculations as
specified by CSS2.

Note that once layout is finished with a line box, we also store the
baseline of the line box in LineBox::m_baseline. This allows us to align
the real baseline of display:inline-block elements with other inline
content on the same line.
2022-03-03 12:57:53 +01:00

610 lines
25 KiB
C++

/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSRule.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/CSSSupportsRule.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/DOM/Comment.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/HTMLTemplateElement.h>
#include <LibWeb/Layout/BlockContainer.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/SVGBox.h>
#include <LibWeb/Layout/TextNode.h>
#include <stdio.h>
namespace Web {
static void indent(StringBuilder& builder, int levels)
{
for (int i = 0; i < levels; i++)
builder.append(" ");
}
void dump_tree(DOM::Node const& node)
{
StringBuilder builder;
dump_tree(builder, node);
dbgln("{}", builder.string_view());
}
void dump_tree(StringBuilder& builder, DOM::Node const& node)
{
static int indent = 0;
for (int i = 0; i < indent; ++i)
builder.append(" ");
if (is<DOM::Element>(node)) {
builder.appendff("<{}", verify_cast<DOM::Element>(node).local_name());
verify_cast<DOM::Element>(node).for_each_attribute([&](auto& name, auto& value) {
builder.appendff(" {}={}", name, value);
});
builder.append(">\n");
} else if (is<DOM::Text>(node)) {
builder.appendff("\"{}\"\n", verify_cast<DOM::Text>(node).data());
} else {
builder.appendff("{}\n", node.node_name());
}
++indent;
if (is<DOM::Element>(node) && verify_cast<DOM::Element>(node).shadow_root()) {
dump_tree(builder, *verify_cast<DOM::Element>(node).shadow_root());
}
if (is<DOM::ParentNode>(node)) {
if (!is<HTML::HTMLTemplateElement>(node)) {
static_cast<DOM::ParentNode const&>(node).for_each_child([&](auto& child) {
dump_tree(builder, child);
});
} else {
auto& template_element = verify_cast<HTML::HTMLTemplateElement>(node);
dump_tree(builder, template_element.content());
}
}
--indent;
}
void dump_tree(Layout::Node const& layout_node, bool show_box_model, bool show_specified_style)
{
StringBuilder builder;
dump_tree(builder, layout_node, show_box_model, show_specified_style, true);
dbgln("{}", builder.string_view());
}
void dump_tree(StringBuilder& builder, Layout::Node const& layout_node, bool show_box_model, bool show_specified_style, bool interactive)
{
static size_t indent = 0;
for (size_t i = 0; i < indent; ++i)
builder.append(" ");
FlyString tag_name;
if (layout_node.is_anonymous())
tag_name = "(anonymous)";
else if (is<DOM::Element>(layout_node.dom_node()))
tag_name = verify_cast<DOM::Element>(*layout_node.dom_node()).local_name();
else
tag_name = layout_node.dom_node()->node_name();
String identifier = "";
if (layout_node.dom_node() && is<DOM::Element>(*layout_node.dom_node())) {
auto& element = verify_cast<DOM::Element>(*layout_node.dom_node());
StringBuilder builder;
auto id = element.attribute(HTML::AttributeNames::id);
if (!id.is_empty()) {
builder.append('#');
builder.append(id);
}
for (auto& class_name : element.class_names()) {
builder.append('.');
builder.append(class_name);
}
identifier = builder.to_string();
}
char const* nonbox_color_on = "";
char const* box_color_on = "";
char const* svg_box_color_on = "";
char const* positioned_color_on = "";
char const* floating_color_on = "";
char const* inline_block_color_on = "";
char const* line_box_color_on = "";
char const* fragment_color_on = "";
char const* flex_color_on = "";
char const* color_off = "";
if (interactive) {
nonbox_color_on = "\033[33m";
box_color_on = "\033[34m";
svg_box_color_on = "\033[31m";
positioned_color_on = "\033[31;1m";
floating_color_on = "\033[32;1m";
inline_block_color_on = "\033[36;1m";
line_box_color_on = "\033[34;1m";
fragment_color_on = "\033[35;1m";
flex_color_on = "\033[34;1m";
color_off = "\033[0m";
}
if (!is<Layout::Box>(layout_node)) {
builder.appendff("{}{}{} <{}{}{}{}>",
nonbox_color_on,
layout_node.class_name().substring_view(13),
color_off,
tag_name,
nonbox_color_on,
identifier,
color_off);
if (interactive)
builder.appendff(" @{:p}", &layout_node);
builder.append("\n");
} else {
auto& box = verify_cast<Layout::Box>(layout_node);
StringView color_on = is<Layout::SVGBox>(box) ? svg_box_color_on : box_color_on;
builder.appendff("{}{}{} <{}{}{}{}> ",
color_on,
box.class_name().substring_view(13),
color_off,
color_on,
tag_name,
color_off,
identifier.characters());
if (interactive)
builder.appendff("@{:p} ", &layout_node);
builder.appendff("at ({},{}) content-size {}x{}",
box.absolute_x(),
box.absolute_y(),
box.content_width(),
box.content_height());
if (box.is_positioned())
builder.appendff(" {}positioned{}", positioned_color_on, color_off);
if (box.is_floating())
builder.appendff(" {}floating{}", floating_color_on, color_off);
if (box.is_inline_block())
builder.appendff(" {}inline-block{}", inline_block_color_on, color_off);
if (box.computed_values().display().is_flex_inside()) {
StringView direction;
switch (box.computed_values().flex_direction()) {
case CSS::FlexDirection::Column:
direction = "column"sv;
break;
case CSS::FlexDirection::ColumnReverse:
direction = "column-reverse"sv;
break;
case CSS::FlexDirection::Row:
direction = "row"sv;
break;
case CSS::FlexDirection::RowReverse:
direction = "row-reverse"sv;
break;
}
builder.appendff(" {}flex-container({}){}", flex_color_on, direction, color_off);
}
if (box.is_flex_item())
builder.appendff(" {}flex-item{}", flex_color_on, color_off);
if (show_box_model) {
// Dump the horizontal box properties
builder.appendff(" [{}+{}+{} {} {}+{}+{}]",
box.box_model().margin.left,
box.box_model().border.left,
box.box_model().padding.left,
box.content_width(),
box.box_model().padding.right,
box.box_model().border.right,
box.box_model().margin.right);
// And the vertical box properties
builder.appendff(" [{}+{}+{} {} {}+{}+{}]",
box.box_model().margin.top,
box.box_model().border.top,
box.box_model().padding.top,
box.content_height(),
box.box_model().padding.bottom,
box.box_model().border.bottom,
box.box_model().margin.bottom);
}
builder.append("\n");
}
if (is<Layout::BlockContainer>(layout_node) && static_cast<Layout::BlockContainer const&>(layout_node).children_are_inline()) {
auto& block = static_cast<Layout::BlockContainer const&>(layout_node);
for (size_t line_box_index = 0; line_box_index < block.line_boxes().size(); ++line_box_index) {
auto& line_box = block.line_boxes()[line_box_index];
for (size_t i = 0; i < indent; ++i)
builder.append(" ");
builder.appendff(" {}line {}{} width: {}, bottom: {}, baseline: {}\n",
line_box_color_on,
line_box_index,
color_off,
line_box.width(),
line_box.bottom(),
line_box.baseline());
for (size_t fragment_index = 0; fragment_index < line_box.fragments().size(); ++fragment_index) {
auto& fragment = line_box.fragments()[fragment_index];
for (size_t i = 0; i < indent; ++i)
builder.append(" ");
builder.appendff(" {}frag {}{} from {} ",
fragment_color_on,
fragment_index,
color_off,
fragment.layout_node().class_name());
if (interactive)
builder.appendff("@{:p}, ", &fragment.layout_node());
builder.appendff("start: {}, length: {}, rect: {}\n",
fragment.start(),
fragment.length(),
fragment.absolute_rect().to_string());
if (is<Layout::TextNode>(fragment.layout_node())) {
for (size_t i = 0; i < indent; ++i)
builder.append(" ");
auto& layout_text = static_cast<Layout::TextNode const&>(fragment.layout_node());
auto fragment_text = layout_text.text_for_rendering().substring(fragment.start(), fragment.length());
builder.appendff(" \"{}\"\n", fragment_text);
}
}
}
}
if (show_specified_style && layout_node.dom_node() && layout_node.dom_node()->is_element() && verify_cast<DOM::Element>(layout_node.dom_node())->specified_css_values()) {
struct NameAndValue {
String name;
String value;
};
Vector<NameAndValue> properties;
verify_cast<DOM::Element>(*layout_node.dom_node()).specified_css_values()->for_each_property([&](auto property_id, auto& value) {
properties.append({ CSS::string_from_property_id(property_id), value.to_string() });
});
quick_sort(properties, [](auto& a, auto& b) { return a.name < b.name; });
for (auto& property : properties) {
for (size_t i = 0; i < indent; ++i)
builder.append(" ");
builder.appendff(" ({}: {})\n", property.name, property.value);
}
}
++indent;
layout_node.for_each_child([&](auto& child) {
dump_tree(builder, child, show_box_model, show_specified_style, interactive);
});
--indent;
}
void dump_selector(CSS::Selector const& selector)
{
StringBuilder builder;
dump_selector(builder, selector);
dbgln("{}", builder.string_view());
}
void dump_selector(StringBuilder& builder, CSS::Selector const& selector)
{
builder.append(" CSS::Selector:\n");
for (auto& relative_selector : selector.compound_selectors()) {
builder.append(" ");
char const* relation_description = "";
switch (relative_selector.combinator) {
case CSS::Selector::Combinator::None:
relation_description = "None";
break;
case CSS::Selector::Combinator::ImmediateChild:
relation_description = "ImmediateChild";
break;
case CSS::Selector::Combinator::Descendant:
relation_description = "Descendant";
break;
case CSS::Selector::Combinator::NextSibling:
relation_description = "AdjacentSibling";
break;
case CSS::Selector::Combinator::SubsequentSibling:
relation_description = "GeneralSibling";
break;
case CSS::Selector::Combinator::Column:
relation_description = "Column";
break;
}
if (*relation_description)
builder.appendff("{{{}}} ", relation_description);
for (size_t i = 0; i < relative_selector.simple_selectors.size(); ++i) {
auto& simple_selector = relative_selector.simple_selectors[i];
char const* type_description = "Unknown";
switch (simple_selector.type) {
case CSS::Selector::SimpleSelector::Type::Invalid:
type_description = "Invalid";
break;
case CSS::Selector::SimpleSelector::Type::Universal:
type_description = "Universal";
break;
case CSS::Selector::SimpleSelector::Type::Id:
type_description = "Id";
break;
case CSS::Selector::SimpleSelector::Type::Class:
type_description = "Class";
break;
case CSS::Selector::SimpleSelector::Type::TagName:
type_description = "TagName";
break;
case CSS::Selector::SimpleSelector::Type::Attribute:
type_description = "Attribute";
break;
case CSS::Selector::SimpleSelector::Type::PseudoClass:
type_description = "PseudoClass";
break;
case CSS::Selector::SimpleSelector::Type::PseudoElement:
type_description = "PseudoElement";
break;
}
builder.appendff("{}:{}", type_description, simple_selector.value);
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoClass) {
auto const& pseudo_class = simple_selector.pseudo_class;
char const* pseudo_class_description = "";
switch (pseudo_class.type) {
case CSS::Selector::SimpleSelector::PseudoClass::Type::Link:
pseudo_class_description = "Link";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Visited:
pseudo_class_description = "Visited";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Active:
pseudo_class_description = "Active";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::None:
pseudo_class_description = "None";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Root:
pseudo_class_description = "Root";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::FirstOfType:
pseudo_class_description = "FirstOfType";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::LastOfType:
pseudo_class_description = "LastOfType";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::OnlyOfType:
pseudo_class_description = "OnlyOfType";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::NthOfType:
pseudo_class_description = "NthOfType";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::NthLastOfType:
pseudo_class_description = "NthLastOfType";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::NthChild:
pseudo_class_description = "NthChild";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::NthLastChild:
pseudo_class_description = "NthLastChild";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Focus:
pseudo_class_description = "Focus";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Empty:
pseudo_class_description = "Empty";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Hover:
pseudo_class_description = "Hover";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::LastChild:
pseudo_class_description = "LastChild";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::FirstChild:
pseudo_class_description = "FirstChild";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::OnlyChild:
pseudo_class_description = "OnlyChild";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Disabled:
pseudo_class_description = "Disabled";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Enabled:
pseudo_class_description = "Enabled";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Checked:
pseudo_class_description = "Checked";
break;
case CSS::Selector::SimpleSelector::PseudoClass::Type::Not:
pseudo_class_description = "Not";
break;
}
builder.appendff(" pseudo_class={}", pseudo_class_description);
if (pseudo_class.type == CSS::Selector::SimpleSelector::PseudoClass::Type::Not) {
builder.append("([");
for (auto& selector : pseudo_class.not_selector)
dump_selector(builder, selector);
builder.append("])");
} else if ((pseudo_class.type == CSS::Selector::SimpleSelector::PseudoClass::Type::NthChild)
|| (pseudo_class.type == CSS::Selector::SimpleSelector::PseudoClass::Type::NthLastChild)) {
builder.appendff("(step={}, offset={})", pseudo_class.nth_child_pattern.step_size, pseudo_class.nth_child_pattern.offset);
}
}
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement) {
char const* pseudo_element_description = "";
switch (simple_selector.pseudo_element) {
case CSS::Selector::PseudoElement::None:
pseudo_element_description = "NONE";
break;
case CSS::Selector::PseudoElement::Before:
pseudo_element_description = "before";
break;
case CSS::Selector::PseudoElement::After:
pseudo_element_description = "after";
break;
case CSS::Selector::PseudoElement::FirstLine:
pseudo_element_description = "first-line";
break;
case CSS::Selector::PseudoElement::FirstLetter:
pseudo_element_description = "first-letter";
break;
case CSS::Selector::PseudoElement::Marker:
pseudo_element_description = "marker";
break;
}
builder.appendff(" pseudo_element={}", pseudo_element_description);
}
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::Attribute) {
char const* attribute_match_type_description = "";
switch (simple_selector.attribute.match_type) {
case CSS::Selector::SimpleSelector::Attribute::MatchType::None:
attribute_match_type_description = "NONE";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute:
attribute_match_type_description = "HasAttribute";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
attribute_match_type_description = "ExactValueMatch";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord:
attribute_match_type_description = "ContainsWord";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString:
attribute_match_type_description = "ContainsString";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment:
attribute_match_type_description = "StartsWithSegment";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
attribute_match_type_description = "StartsWithString";
break;
case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
attribute_match_type_description = "EndsWithString";
break;
}
builder.appendff(" [{}, name='{}', value='{}']", attribute_match_type_description, simple_selector.attribute.name, simple_selector.attribute.value);
}
if (i != relative_selector.simple_selectors.size() - 1)
builder.append(", ");
}
builder.append("\n");
}
}
void dump_rule(CSS::CSSRule const& rule)
{
StringBuilder builder;
dump_rule(builder, rule);
dbgln("{}", builder.string_view());
}
void dump_rule(StringBuilder& builder, CSS::CSSRule const& rule, int indent_levels)
{
indent(builder, indent_levels);
builder.appendff("{}:\n", rule.class_name());
switch (rule.type()) {
case CSS::CSSRule::Type::Style:
dump_style_rule(builder, verify_cast<CSS::CSSStyleRule const>(rule), indent_levels);
break;
case CSS::CSSRule::Type::Import:
dump_import_rule(builder, verify_cast<CSS::CSSImportRule const>(rule), indent_levels);
break;
case CSS::CSSRule::Type::Media:
dump_media_rule(builder, verify_cast<CSS::CSSMediaRule const>(rule), indent_levels);
break;
case CSS::CSSRule::Type::Supports:
dump_supports_rule(builder, verify_cast<CSS::CSSSupportsRule const>(rule), indent_levels);
break;
case CSS::CSSRule::Type::__Count:
VERIFY_NOT_REACHED();
}
}
void dump_import_rule(StringBuilder& builder, CSS::CSSImportRule const& rule, int indent_levels)
{
indent(builder, indent_levels);
builder.appendff(" Document URL: {}\n", rule.url());
}
void dump_media_rule(StringBuilder& builder, CSS::CSSMediaRule const& media, int indent_levels)
{
indent(builder, indent_levels);
builder.appendff(" Media: {}\n Rules ({}):\n", media.condition_text(), media.css_rules().length());
for (auto& rule : media.css_rules()) {
dump_rule(builder, rule, indent_levels + 1);
}
}
void dump_supports_rule(StringBuilder& builder, CSS::CSSSupportsRule const& supports, int indent_levels)
{
indent(builder, indent_levels);
builder.appendff(" Supports: {}\n Rules ({}):\n", supports.condition_text(), supports.css_rules().length());
for (auto& rule : supports.css_rules()) {
dump_rule(builder, rule, indent_levels + 1);
}
}
void dump_style_rule(StringBuilder& builder, CSS::CSSStyleRule const& rule, int indent_levels)
{
for (auto& selector : rule.selectors()) {
dump_selector(builder, selector);
}
indent(builder, indent_levels);
builder.append(" Declarations:\n");
auto& style_declaration = verify_cast<CSS::PropertyOwningCSSStyleDeclaration>(rule.declaration());
for (auto& property : style_declaration.properties()) {
indent(builder, indent_levels);
builder.appendff(" {}: '{}'", CSS::string_from_property_id(property.property_id), property.value->to_string());
if (property.important == CSS::Important::Yes)
builder.append(" \033[31;1m!important\033[0m");
builder.append('\n');
}
for (auto& property : style_declaration.custom_properties()) {
indent(builder, indent_levels);
builder.appendff(" {}: '{}'", property.key, property.value.value->to_string());
if (property.value.important == CSS::Important::Yes)
builder.append(" \033[31;1m!important\033[0m");
builder.append('\n');
}
}
void dump_sheet(CSS::StyleSheet const& sheet)
{
StringBuilder builder;
dump_sheet(builder, sheet);
dbgln("{}", builder.string_view());
}
void dump_sheet(StringBuilder& builder, CSS::StyleSheet const& sheet)
{
auto& css_stylesheet = verify_cast<CSS::CSSStyleSheet>(sheet);
builder.appendff("CSSStyleSheet{{{}}}: {} rule(s)\n", &sheet, css_stylesheet.rules().length());
for (auto& rule : css_stylesheet.rules()) {
dump_rule(builder, rule);
}
}
}