Skip to content

Commit

Permalink
LibWeb: Rework obtaining resolved styles in the Editing API
Browse files Browse the repository at this point in the history
The algorithm referenced to in the Editing spec whenever they talk about
obtaining the "resolved" style or value is actually implemented in
ResolvedCSSStyleDeclaration, so use that instead of going directly to
the computed styles.
  • Loading branch information
gmta committed Dec 19, 2024
1 parent 63f015b commit 4677ac2
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 35 deletions.
8 changes: 5 additions & 3 deletions Libraries/LibWeb/Editing/Commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/DOM/Comment.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentFragment.h>
Expand Down Expand Up @@ -417,10 +418,11 @@ bool command_insert_linebreak_action(DOM::Document& document, String const&)
// * Insert another newline (\n) character if the active range's start offset is equal to the length of the
// active range's start node.
// * Return true.
if (is<DOM::Text>(*start_node) && start_node->layout_node()) {
if (is<DOM::Text>(*start_node)) {
auto& text_node = static_cast<DOM::Text&>(*start_node);
auto white_space = text_node.layout_node()->computed_values().white_space();
if (first_is_one_of(white_space, CSS::WhiteSpace::Pre, CSS::WhiteSpace::PreLine, CSS::WhiteSpace::PreWrap)) {
auto resolved_white_space = resolved_keyword(*start_node, CSS::PropertyID::WhiteSpace);
if (resolved_white_space.has_value()
&& first_is_one_of(resolved_white_space.value(), CSS::Keyword::Pre, CSS::Keyword::PreLine, CSS::Keyword::PreWrap)) {
MUST(text_node.insert_data(active_range.start_offset(), "\n"_string));
MUST(selection.collapse(start_node, active_range.start_offset() + 1));
if (selection.range()->start_offset() == start_node->length())
Expand Down
92 changes: 60 additions & 32 deletions Libraries/LibWeb/Editing/Internal/Algorithms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <LibWeb/CSS/ResolvedCSSStyleDeclaration.h>
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/DocumentFragment.h>
#include <LibWeb/DOM/DocumentType.h>
Expand Down Expand Up @@ -250,15 +252,14 @@ void canonicalize_whitespace(GC::Ref<DOM::Node> node, u32 offset, bool fix_colla
// "white-space" is neither "pre" nor "pre-wrap" and start offset is not zero and the
// (start offset − 1)st code unit of start node's data is a space (0x0020) or
// non-breaking space (0x00A0), subtract one from start offset.
auto* layout_node = start_node->parent()->layout_node();
if (layout_node && is<DOM::Text>(*start_node) && start_offset != 0) {
auto parent_white_space = layout_node->computed_values().white_space();
if (is<DOM::Text>(*start_node) && start_offset != 0) {
auto parent_white_space = resolved_keyword(*start_node->parent(), CSS::PropertyID::WhiteSpace);

// FIXME: Find a way to get code points directly from the UTF-8 string
auto start_node_data = *start_node->text_content();
auto utf16_code_units = MUST(AK::utf8_to_utf16(start_node_data));
auto offset_minus_one_code_point = Utf16View { utf16_code_units }.code_point_at(start_offset - 1);
if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
if (parent_white_space != CSS::Keyword::Pre && parent_white_space != CSS::Keyword::PreWrap
&& (offset_minus_one_code_point == 0x20 || offset_minus_one_code_point == 0xA0)) {
--start_offset;
continue;
Expand Down Expand Up @@ -304,15 +305,14 @@ void canonicalize_whitespace(GC::Ref<DOM::Node> node, u32 offset, bool fix_colla
// "white-space" is neither "pre" nor "pre-wrap" and end offset is not end node's length
// and the end offsetth code unit of end node's data is a space (0x0020) or non-breaking
// space (0x00A0):
auto* layout_node = end_node->parent()->layout_node();
if (layout_node && is<DOM::Text>(*end_node) && end_offset != end_node->length()) {
auto parent_white_space = layout_node->computed_values().white_space();
if (is<DOM::Text>(*end_node) && end_offset != end_node->length()) {
auto parent_white_space = resolved_keyword(*end_node->parent(), CSS::PropertyID::WhiteSpace);

// FIXME: Find a way to get code points directly from the UTF-8 string
auto end_node_data = *end_node->text_content();
auto utf16_code_units = MUST(AK::utf8_to_utf16(end_node_data));
auto offset_code_point = Utf16View { utf16_code_units }.code_point_at(end_offset);
if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
if (parent_white_space != CSS::Keyword::Pre && parent_white_space != CSS::Keyword::PreWrap
&& (offset_code_point == 0x20 || offset_code_point == 0xA0)) {
// 1. If fix collapsed space is true, and collapse spaces is true, and the end offsetth
// code unit of end node's data is a space (0x0020): call deleteData(end offset, 1)
Expand Down Expand Up @@ -370,10 +370,9 @@ void canonicalize_whitespace(GC::Ref<DOM::Node> node, u32 offset, bool fix_colla
// "white-space" is neither "pre" nor "pre-wrap" and end offset is end node's length and
// the last code unit of end node's data is a space (0x0020) and end node precedes a line
// break:
auto* layout_node = end_node->parent()->layout_node();
if (layout_node && is<DOM::Text>(*end_node) && end_offset == end_node->length() && precedes_a_line_break(end_node)) {
auto parent_white_space = layout_node->computed_values().white_space();
if (parent_white_space != CSS::WhiteSpace::Pre && parent_white_space != CSS::WhiteSpace::PreWrap
if (is<DOM::Text>(*end_node) && end_offset == end_node->length() && precedes_a_line_break(end_node)) {
auto parent_white_space = resolved_keyword(*end_node->parent(), CSS::PropertyID::WhiteSpace);
if (parent_white_space != CSS::Keyword::Pre && parent_white_space != CSS::Keyword::PreWrap
&& end_node->text_content().value().ends_with_bytes(" "sv)) {
// 1. Subtract one from end offset.
--end_offset;
Expand Down Expand Up @@ -1267,14 +1266,11 @@ bool is_block_node(GC::Ref<DOM::Node> node)
if (is<DOM::Document>(*node) || is<DOM::DocumentFragment>(*node))
return true;

auto layout_node = node->layout_node();
if (!layout_node)
if (!is<DOM::Element>(*node))
return false;

auto display = layout_node->display();
return is<DOM::Element>(*node)
&& !(display.is_inline_outside() && (display.is_flow_inside() || display.is_flow_root_inside() || display.is_table_inside()))
&& !display.is_none();
auto display_keyword = resolved_keyword(node, CSS::PropertyID::Display);
return !display_keyword.has_value()
|| !first_is_one_of(display_keyword.value(), CSS::Keyword::Inline, CSS::Keyword::InlineBlock, CSS::Keyword::InlineTable, CSS::Keyword::None);
}

// https://w3c.github.io/editing/docs/execCommand/#block-start-point
Expand Down Expand Up @@ -1364,8 +1360,15 @@ bool is_collapsed_whitespace_node(GC::Ref<DOM::Node> node)
return true;

// 5. If the "display" property of some ancestor of node has resolved value "none", return true.
if (ancestor->layout_node() && ancestor->layout_node()->display().is_none())
return true;
GC::Ptr<DOM::Node> some_ancestor = node->parent();
while (some_ancestor) {
if (is<DOM::Element>(*some_ancestor)) {
auto resolved_display = resolved_keyword(*some_ancestor, CSS::PropertyID::Display);
if (resolved_display.has_value() && resolved_display.value() == CSS::Keyword::None)
return true;
}
some_ancestor = some_ancestor->parent();
}

// 6. While ancestor is not a block node and its parent is not null, set ancestor to its parent.
while (!is_block_node(*ancestor) && ancestor->parent())
Expand Down Expand Up @@ -1615,8 +1618,8 @@ bool is_visible_node(GC::Ref<DOM::Node> node)
// value "none".
GC::Ptr<DOM::Node> inclusive_ancestor = node;
while (inclusive_ancestor) {
auto* layout_node = inclusive_ancestor->layout_node();
if (layout_node && layout_node->display().is_none())
auto resolved_display = resolved_keyword(*inclusive_ancestor, CSS::PropertyID::Display);
if (resolved_display.has_value() && resolved_display.value() == CSS::Keyword::None)
return false;
inclusive_ancestor = inclusive_ancestor->parent();
}
Expand Down Expand Up @@ -1666,10 +1669,10 @@ bool is_whitespace_node(GC::Ref<DOM::Node> node)
GC::Ptr<DOM::Node> parent = node->parent();
if (!is<DOM::Element>(parent.ptr()))
return false;
auto* layout_node = parent->layout_node();
if (!layout_node)
auto resolved_white_space = resolved_keyword(*parent, CSS::PropertyID::WhiteSpace);
if (!resolved_white_space.has_value())
return false;
auto white_space = layout_node->computed_values().white_space();
auto white_space = resolved_white_space.value();

// or a Text node whose data consists only of one or more tabs (0x0009), line feeds (0x000A),
// carriage returns (0x000D), and/or spaces (0x0020), and whose parent is an Element whose
Expand All @@ -1678,7 +1681,7 @@ bool is_whitespace_node(GC::Ref<DOM::Node> node)
return codepoint == '\t' || codepoint == '\n' || codepoint == '\r' || codepoint == ' ';
};
auto code_points = character_data.data().code_points();
if (all_of(code_points, is_tab_lf_cr_or_space) && (white_space == CSS::WhiteSpace::Normal || white_space == CSS::WhiteSpace::Nowrap))
if (all_of(code_points, is_tab_lf_cr_or_space) && (white_space == CSS::Keyword::Normal || white_space == CSS::Keyword::Nowrap))
return true;

// or a Text node whose data consists only of one or more tabs (0x0009), carriage returns
Expand All @@ -1687,7 +1690,7 @@ bool is_whitespace_node(GC::Ref<DOM::Node> node)
auto is_tab_cr_or_space = [](u32 codepoint) {
return codepoint == '\t' || codepoint == '\r' || codepoint == ' ';
};
if (all_of(code_points, is_tab_cr_or_space) && white_space == CSS::WhiteSpace::PreLine)
if (all_of(code_points, is_tab_cr_or_space) && white_space == CSS::Keyword::PreLine)
return true;

return false;
Expand Down Expand Up @@ -2116,10 +2119,10 @@ GC::Ref<DOM::Element> set_the_tag_name(GC::Ref<DOM::Element> element, FlyString
// https://w3c.github.io/editing/docs/execCommand/#specified-command-value
Optional<String> specified_command_value(GC::Ref<DOM::Element> element, FlyString const& command)
{
// 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value "inline", return null.
auto layout_node = element->layout_node();
if ((command == CommandNames::backColor || command == CommandNames::hiliteColor) && layout_node) {
if (layout_node->computed_values().display().is_inline_outside())
// 1. If command is "backColor" or "hiliteColor" and the Element's display property does not have resolved value
// "inline", return null.
if (command == CommandNames::backColor || command == CommandNames::hiliteColor) {
if (resolved_keyword(element, CSS::PropertyID::Display) != CSS::Keyword::Inline)
return {};
}

Expand Down Expand Up @@ -2518,4 +2521,29 @@ bool is_heading(FlyString const& local_name)
HTML::TagNames::h6);
}

Optional<CSS::Keyword> resolved_keyword(GC::Ref<DOM::Node> node, CSS::PropertyID property_id)
{
auto resolved_property = resolved_value(node, property_id);
if (!resolved_property.has_value() || !resolved_property.value()->is_keyword())
return {};
return resolved_property.value()->as_keyword().keyword();
}

Optional<NonnullRefPtr<CSS::CSSStyleValue const>> resolved_value(GC::Ref<DOM::Node> node, CSS::PropertyID property_id)
{
// Find the nearest inclusive ancestor of node that is an Element
GC::Ptr<DOM::Node> element = node;
while (element && !is<DOM::Element>(*element))
element = element->parent();
if (!element)
return {};

// Retrieve resolved style value
auto resolved_css_style_declaration = CSS::ResolvedCSSStyleDeclaration::create(static_cast<DOM::Element&>(*element));
auto optional_style_property = resolved_css_style_declaration->property(property_id);
if (!optional_style_property.has_value())
return {};
return optional_style_property.value().value;
}

}
2 changes: 2 additions & 0 deletions Libraries/LibWeb/Editing/Internal/Algorithms.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,7 @@ GC::Ptr<DOM::Node> wrap(Vector<GC::Ref<DOM::Node>>, Function<bool(GC::Ref<DOM::N

bool has_visible_children(GC::Ref<DOM::Node>);
bool is_heading(FlyString const&);
Optional<CSS::Keyword> resolved_keyword(GC::Ref<DOM::Node>, CSS::PropertyID);
Optional<NonnullRefPtr<CSS::CSSStyleValue const>> resolved_value(GC::Ref<DOM::Node>, CSS::PropertyID);

}

0 comments on commit 4677ac2

Please sign in to comment.