mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 12:19:54 +00:00
LibWeb: Implement "preserves overrides" property of editing commands
This commit is contained in:
parent
e21ee10b3c
commit
2b6a14c5ee
Notes:
github-actions[bot]
2025-01-10 22:38:22 +00:00
Author: https://github.com/gmta
Commit: 2b6a14c5ee
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3216
9 changed files with 315 additions and 80 deletions
|
@ -1774,39 +1774,15 @@ RefPtr<Gfx::FontCascadeList const> StyleComputer::font_matching_algorithm(FlyStr
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
RefPtr<Gfx::FontCascadeList const> StyleComputer::compute_font_for_style_values(DOM::Element const* element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, CSSStyleValue const& font_family, CSSStyleValue const& font_size, CSSStyleValue const& font_style, CSSStyleValue const& font_weight, CSSStyleValue const& font_stretch, int math_depth) const
|
CSSPixels StyleComputer::default_user_font_size()
|
||||||
{
|
{
|
||||||
auto* parent_element = element_to_inherit_style_from(element, pseudo_element);
|
// FIXME: This value should be configurable by the user.
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
|
||||||
auto width = font_stretch.to_font_width();
|
// https://w3c.github.io/csswg-drafts/css-fonts/#absolute-size-mapping
|
||||||
auto weight = font_weight.to_font_weight();
|
CSSPixelFraction StyleComputer::absolute_size_mapping(Keyword keyword)
|
||||||
|
{
|
||||||
// FIXME: Should be based on "user's default font size"
|
|
||||||
CSSPixels font_size_in_px = 16;
|
|
||||||
|
|
||||||
Gfx::FontPixelMetrics font_pixel_metrics;
|
|
||||||
if (parent_element && parent_element->computed_properties())
|
|
||||||
font_pixel_metrics = parent_element->computed_properties()->first_available_computed_font().pixel_metrics();
|
|
||||||
else
|
|
||||||
font_pixel_metrics = Platform::FontPlugin::the().default_font(font_size_in_px.to_float())->pixel_metrics();
|
|
||||||
auto parent_font_size = [&]() -> CSSPixels {
|
|
||||||
if (!parent_element || !parent_element->computed_properties())
|
|
||||||
return font_size_in_px;
|
|
||||||
auto const& value = parent_element->computed_properties()->property(CSS::PropertyID::FontSize);
|
|
||||||
if (value.is_length()) {
|
|
||||||
auto length = value.as_length().length();
|
|
||||||
if (length.is_absolute() || length.is_relative()) {
|
|
||||||
Length::FontMetrics font_metrics { font_size_in_px, font_pixel_metrics };
|
|
||||||
return length.to_px(viewport_rect(), font_metrics, m_root_element_font_metrics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return font_size_in_px;
|
|
||||||
};
|
|
||||||
Length::FontMetrics font_metrics { parent_font_size(), font_pixel_metrics };
|
|
||||||
|
|
||||||
if (font_size.is_keyword()) {
|
|
||||||
// https://w3c.github.io/csswg-drafts/css-fonts/#absolute-size-mapping
|
|
||||||
auto get_absolute_size_mapping = [](Keyword keyword) -> CSSPixelFraction {
|
|
||||||
switch (keyword) {
|
switch (keyword) {
|
||||||
case Keyword::XxSmall:
|
case Keyword::XxSmall:
|
||||||
return CSSPixels(3) / 5;
|
return CSSPixels(3) / 5;
|
||||||
|
@ -1831,8 +1807,38 @@ RefPtr<Gfx::FontCascadeList const> StyleComputer::compute_font_for_style_values(
|
||||||
default:
|
default:
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
RefPtr<Gfx::FontCascadeList const> StyleComputer::compute_font_for_style_values(DOM::Element const* element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, CSSStyleValue const& font_family, CSSStyleValue const& font_size, CSSStyleValue const& font_style, CSSStyleValue const& font_weight, CSSStyleValue const& font_stretch, int math_depth) const
|
||||||
|
{
|
||||||
|
auto* parent_element = element_to_inherit_style_from(element, pseudo_element);
|
||||||
|
|
||||||
|
auto width = font_stretch.to_font_width();
|
||||||
|
auto weight = font_weight.to_font_weight();
|
||||||
|
|
||||||
|
auto font_size_in_px = default_user_font_size();
|
||||||
|
|
||||||
|
Gfx::FontPixelMetrics font_pixel_metrics;
|
||||||
|
if (parent_element && parent_element->computed_properties())
|
||||||
|
font_pixel_metrics = parent_element->computed_properties()->first_available_computed_font().pixel_metrics();
|
||||||
|
else
|
||||||
|
font_pixel_metrics = Platform::FontPlugin::the().default_font(font_size_in_px.to_float())->pixel_metrics();
|
||||||
|
auto parent_font_size = [&]() -> CSSPixels {
|
||||||
|
if (!parent_element || !parent_element->computed_properties())
|
||||||
|
return font_size_in_px;
|
||||||
|
auto const& value = parent_element->computed_properties()->property(CSS::PropertyID::FontSize);
|
||||||
|
if (value.is_length()) {
|
||||||
|
auto length = value.as_length().length();
|
||||||
|
if (length.is_absolute() || length.is_relative()) {
|
||||||
|
Length::FontMetrics font_metrics { font_size_in_px, font_pixel_metrics };
|
||||||
|
return length.to_px(viewport_rect(), font_metrics, m_root_element_font_metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return font_size_in_px;
|
||||||
|
};
|
||||||
|
Length::FontMetrics font_metrics { parent_font_size(), font_pixel_metrics };
|
||||||
|
|
||||||
|
if (font_size.is_keyword()) {
|
||||||
auto const keyword = font_size.to_keyword();
|
auto const keyword = font_size.to_keyword();
|
||||||
|
|
||||||
if (keyword == Keyword::Math) {
|
if (keyword == Keyword::Math) {
|
||||||
|
@ -1885,7 +1891,7 @@ RefPtr<Gfx::FontCascadeList const> StyleComputer::compute_font_for_style_values(
|
||||||
font_size_in_px = CSSPixels::nearest_value_for(parent_element->computed_properties()->first_available_computed_font().pixel_metrics().size);
|
font_size_in_px = CSSPixels::nearest_value_for(parent_element->computed_properties()->first_available_computed_font().pixel_metrics().size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
font_size_in_px *= get_absolute_size_mapping(keyword);
|
font_size_in_px *= absolute_size_mapping(keyword);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Length::ResolutionContext const length_resolution_context {
|
Length::ResolutionContext const length_resolution_context {
|
||||||
|
|
|
@ -160,6 +160,8 @@ public:
|
||||||
void load_fonts_from_sheet(CSSStyleSheet&);
|
void load_fonts_from_sheet(CSSStyleSheet&);
|
||||||
void unload_fonts_from_sheet(CSSStyleSheet&);
|
void unload_fonts_from_sheet(CSSStyleSheet&);
|
||||||
|
|
||||||
|
static CSSPixels default_user_font_size();
|
||||||
|
static CSSPixelFraction absolute_size_mapping(Keyword);
|
||||||
RefPtr<Gfx::FontCascadeList const> compute_font_for_style_values(DOM::Element const* element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, CSSStyleValue const& font_family, CSSStyleValue const& font_size, CSSStyleValue const& font_style, CSSStyleValue const& font_weight, CSSStyleValue const& font_stretch, int math_depth = 0) const;
|
RefPtr<Gfx::FontCascadeList const> compute_font_for_style_values(DOM::Element const* element, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, CSSStyleValue const& font_family, CSSStyleValue const& font_size, CSSStyleValue const& font_style, CSSStyleValue const& font_weight, CSSStyleValue const& font_stretch, int math_depth = 0) const;
|
||||||
|
|
||||||
void set_viewport_rect(Badge<DOM::Document>, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; }
|
void set_viewport_rect(Badge<DOM::Document>, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; }
|
||||||
|
|
|
@ -791,6 +791,7 @@ static Array const commands {
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
.command = CommandNames::delete_,
|
.command = CommandNames::delete_,
|
||||||
.action = command_delete_action,
|
.action = command_delete_action,
|
||||||
|
.preserves_overrides = true,
|
||||||
},
|
},
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
|
// https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
|
@ -802,11 +803,13 @@ static Array const commands {
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
.command = CommandNames::insertLineBreak,
|
.command = CommandNames::insertLineBreak,
|
||||||
.action = command_insert_linebreak_action,
|
.action = command_insert_linebreak_action,
|
||||||
|
.preserves_overrides = true,
|
||||||
},
|
},
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#the-insertparagraph-command
|
// https://w3c.github.io/editing/docs/execCommand/#the-insertparagraph-command
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
.command = CommandNames::insertParagraph,
|
.command = CommandNames::insertParagraph,
|
||||||
.action = command_insert_paragraph_action,
|
.action = command_insert_paragraph_action,
|
||||||
|
.preserves_overrides = true,
|
||||||
},
|
},
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
|
// https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
|
||||||
CommandDefinition {
|
CommandDefinition {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
|
* Copyright (c) 2024-2025, Jelle Raaijmakers <jelle@ladybird.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -19,6 +19,9 @@ struct CommandDefinition {
|
||||||
Function<String(DOM::Document const&)> value {};
|
Function<String(DOM::Document const&)> value {};
|
||||||
Optional<CSS::PropertyID> relevant_css_property {};
|
Optional<CSS::PropertyID> relevant_css_property {};
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#preserves-overrides
|
||||||
|
bool preserves_overrides { false };
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values
|
// https://w3c.github.io/editing/docs/execCommand/#inline-command-activated-values
|
||||||
Vector<String> inline_activated_values {};
|
Vector<String> inline_activated_values {};
|
||||||
};
|
};
|
||||||
|
|
|
@ -69,12 +69,22 @@ bool Document::exec_command(FlyString const& command, [[maybe_unused]] bool show
|
||||||
// at the editing host that was actually affected.
|
// at the editing host that was actually affected.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#preserves-overrides
|
||||||
|
// If a command preserves overrides, then before taking its action, the user agent must record current overrides.
|
||||||
|
auto overrides = Editing::record_current_overrides(*this);
|
||||||
|
|
||||||
// 5. Take the action for command, passing value to the instructions as an argument.
|
// 5. Take the action for command, passing value to the instructions as an argument.
|
||||||
auto optional_command = Editing::find_command_definition(command);
|
auto optional_command = Editing::find_command_definition(command);
|
||||||
VERIFY(optional_command.has_value());
|
VERIFY(optional_command.has_value());
|
||||||
auto const& command_definition = optional_command.release_value();
|
auto const& command_definition = optional_command.release_value();
|
||||||
auto command_result = command_definition.action(*this, value);
|
auto command_result = command_definition.action(*this, value);
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#preserves-overrides
|
||||||
|
// After taking the action, if the active range is collapsed, it must restore states and values from the recorded
|
||||||
|
// list.
|
||||||
|
if (m_selection && m_selection->is_collapsed())
|
||||||
|
Editing::restore_states_and_values(*this, overrides);
|
||||||
|
|
||||||
// 6. If the previous step returned false, return false.
|
// 6. If the previous step returned false, return false.
|
||||||
if (!command_result)
|
if (!command_result)
|
||||||
return false;
|
return false;
|
||||||
|
@ -303,8 +313,12 @@ String Document::query_command_value(FlyString const& command)
|
||||||
if (!command_definition.value && !value_override.has_value())
|
if (!command_definition.value && !value_override.has_value())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
// FIXME: 2. If command is "fontSize" and its value override is set, convert the value override to an
|
// 2. If command is "fontSize" and its value override is set, convert the value override to an
|
||||||
// integer number of pixels and return the legacy font size for the result.
|
// integer number of pixels and return the legacy font size for the result.
|
||||||
|
if (command == Editing::CommandNames::fontSize && value_override.has_value()) {
|
||||||
|
auto pixel_size = Editing::font_size_to_pixel_size(value_override.release_value());
|
||||||
|
return Editing::legacy_font_size(pixel_size.to_int());
|
||||||
|
}
|
||||||
|
|
||||||
// 3. If the value override for command is set, return it.
|
// 3. If the value override for command is set, return it.
|
||||||
if (value_override.has_value())
|
if (value_override.has_value())
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <LibGfx/Color.h>
|
||||||
#include <LibWeb/CSS/ResolvedCSSStyleDeclaration.h>
|
#include <LibWeb/CSS/ResolvedCSSStyleDeclaration.h>
|
||||||
|
#include <LibWeb/CSS/StyleComputer.h>
|
||||||
#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
|
#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
||||||
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
#include <LibWeb/HTML/HTMLAnchorElement.h>
|
#include <LibWeb/HTML/HTMLAnchorElement.h>
|
||||||
#include <LibWeb/HTML/HTMLBRElement.h>
|
#include <LibWeb/HTML/HTMLBRElement.h>
|
||||||
#include <LibWeb/HTML/HTMLElement.h>
|
#include <LibWeb/HTML/HTMLElement.h>
|
||||||
|
#include <LibWeb/HTML/HTMLFontElement.h>
|
||||||
#include <LibWeb/HTML/HTMLImageElement.h>
|
#include <LibWeb/HTML/HTMLImageElement.h>
|
||||||
#include <LibWeb/HTML/HTMLLIElement.h>
|
#include <LibWeb/HTML/HTMLLIElement.h>
|
||||||
#include <LibWeb/HTML/HTMLOListElement.h>
|
#include <LibWeb/HTML/HTMLOListElement.h>
|
||||||
|
@ -561,7 +564,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
end_block = {};
|
end_block = {};
|
||||||
|
|
||||||
// 19. Record current states and values, and let overrides be the result.
|
// 19. Record current states and values, and let overrides be the result.
|
||||||
auto overrides = record_current_states_and_values(*active_range(document));
|
auto overrides = record_current_states_and_values(document);
|
||||||
|
|
||||||
// 21. If start node and end node are the same, and start node is an editable Text node:
|
// 21. If start node and end node are the same, and start node is an editable Text node:
|
||||||
if (start.node == end.node && is<DOM::Text>(*start.node) && start.node->is_editable()) {
|
if (start.node == end.node && is<DOM::Text>(*start.node) && start.node->is_editable()) {
|
||||||
|
@ -582,7 +585,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Restore states and values from overrides.
|
// 5. Restore states and values from overrides.
|
||||||
restore_states_and_values(*selection.range(), overrides);
|
restore_states_and_values(document, overrides);
|
||||||
|
|
||||||
// 6. Abort these steps.
|
// 6. Abort these steps.
|
||||||
return;
|
return;
|
||||||
|
@ -662,7 +665,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Restore states and values from overrides.
|
// 3. Restore states and values from overrides.
|
||||||
restore_states_and_values(*selection.range(), overrides);
|
restore_states_and_values(document, overrides);
|
||||||
|
|
||||||
// 4. Abort these steps.
|
// 4. Abort these steps.
|
||||||
return;
|
return;
|
||||||
|
@ -712,7 +715,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
end_block->remove();
|
end_block->remove();
|
||||||
|
|
||||||
// 4. Restore states and values from overrides.
|
// 4. Restore states and values from overrides.
|
||||||
restore_states_and_values(*active_range(document), overrides);
|
restore_states_and_values(document, overrides);
|
||||||
|
|
||||||
// 5. Abort these steps.
|
// 5. Abort these steps.
|
||||||
return;
|
return;
|
||||||
|
@ -721,7 +724,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
// 5. If end block's firstChild is not an inline node, restore states and values from record, then abort these
|
// 5. If end block's firstChild is not an inline node, restore states and values from record, then abort these
|
||||||
// steps.
|
// steps.
|
||||||
if (!is_inline_node(*end_block->first_child())) {
|
if (!is_inline_node(*end_block->first_child())) {
|
||||||
restore_states_and_values(*active_range(document), overrides);
|
restore_states_and_values(document, overrides);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -883,7 +886,7 @@ void delete_the_selection(Selection& selection, bool block_merging, bool strip_w
|
||||||
remove_extraneous_line_breaks_at_the_end_of_node(*start_block);
|
remove_extraneous_line_breaks_at_the_end_of_node(*start_block);
|
||||||
|
|
||||||
// 41. Restore states and values from overrides.
|
// 41. Restore states and values from overrides.
|
||||||
restore_states_and_values(*active_range(document), overrides);
|
restore_states_and_values(document, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#editing-host-of
|
// https://w3c.github.io/editing/docs/execCommand/#editing-host-of
|
||||||
|
@ -1917,6 +1920,37 @@ DOM::BoundaryPoint last_equivalent_point(DOM::BoundaryPoint boundary_point)
|
||||||
return boundary_point;
|
return boundary_point;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#legacy-font-size-for
|
||||||
|
String legacy_font_size(int pixel_size)
|
||||||
|
{
|
||||||
|
// 1. Let returned size be 1.
|
||||||
|
auto returned_size = 1;
|
||||||
|
|
||||||
|
// 2. While returned size is less than 7:
|
||||||
|
while (returned_size < 7) {
|
||||||
|
// 1. Let lower bound be the resolved value of "font-size" in pixels of a font element whose size attribute is
|
||||||
|
// set to returned size.
|
||||||
|
auto lower_bound = font_size_to_pixel_size(MUST(String::formatted("{}", returned_size))).to_float();
|
||||||
|
|
||||||
|
// 2. Let upper bound be the resolved value of "font-size" in pixels of a font element whose size attribute is
|
||||||
|
// set to one plus returned size.
|
||||||
|
auto upper_bound = font_size_to_pixel_size(MUST(String::formatted("{}", returned_size + 1))).to_float();
|
||||||
|
|
||||||
|
// 3. Let average be the average of upper bound and lower bound.
|
||||||
|
auto average = (lower_bound + upper_bound) / 2;
|
||||||
|
|
||||||
|
// 4. If pixel size is less than average, return the one-code unit string consisting of the digit returned size.
|
||||||
|
if (pixel_size < average)
|
||||||
|
return MUST(String::formatted("{}", returned_size));
|
||||||
|
|
||||||
|
// 5. Add one to returned size.
|
||||||
|
++returned_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Return "7".
|
||||||
|
return "7"_string;
|
||||||
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#preserving-ranges
|
// https://w3c.github.io/editing/docs/execCommand/#preserving-ranges
|
||||||
void move_node_preserving_ranges(GC::Ref<DOM::Node> node, GC::Ref<DOM::Node> new_parent, u32 new_index)
|
void move_node_preserving_ranges(GC::Ref<DOM::Node> node, GC::Ref<DOM::Node> new_parent, u32 new_index)
|
||||||
{
|
{
|
||||||
|
@ -2075,22 +2109,45 @@ Optional<DOM::BoundaryPoint> previous_equivalent_point(DOM::BoundaryPoint bounda
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#record-current-overrides
|
||||||
|
Vector<RecordedOverride> record_current_overrides(DOM::Document const& document)
|
||||||
|
{
|
||||||
|
// 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
|
||||||
|
Vector<RecordedOverride> overrides;
|
||||||
|
|
||||||
|
// 2. If there is a value override for "createLink", add ("createLink", value override for "createLink") to
|
||||||
|
// overrides.
|
||||||
|
if (auto override = document.command_value_override(CommandNames::createLink); override.has_value())
|
||||||
|
overrides.empend(CommandNames::createLink, override.release_value());
|
||||||
|
|
||||||
|
// 3. For each command in the list "bold", "italic", "strikethrough", "subscript", "superscript", "underline", in
|
||||||
|
// order: if there is a state override for command, add (command, command's state override) to overrides.
|
||||||
|
for (auto const& command : { CommandNames::bold, CommandNames::italic, CommandNames::strikethrough,
|
||||||
|
CommandNames::subscript, CommandNames::superscript, CommandNames::underline }) {
|
||||||
|
if (auto override = document.command_state_override(command); override.has_value())
|
||||||
|
overrides.empend(command, override.release_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. For each command in the list "fontName", "fontSize", "foreColor", "hiliteColor", in order: if there is a value
|
||||||
|
// override for command, add (command, command's value override) to overrides.
|
||||||
|
for (auto const& command : { CommandNames::fontName, CommandNames::fontSize, CommandNames::foreColor,
|
||||||
|
CommandNames::hiliteColor }) {
|
||||||
|
if (auto override = document.command_value_override(command); override.has_value())
|
||||||
|
overrides.empend(command, override.release_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Return overrides.
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values
|
// https://w3c.github.io/editing/docs/execCommand/#record-current-states-and-values
|
||||||
Vector<RecordedOverride> record_current_states_and_values(GC::Ref<DOM::Range> active_range)
|
Vector<RecordedOverride> record_current_states_and_values(DOM::Document const& document)
|
||||||
{
|
{
|
||||||
// 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
|
// 1. Let overrides be a list of (string, string or boolean) ordered pairs, initially empty.
|
||||||
Vector<RecordedOverride> overrides;
|
Vector<RecordedOverride> overrides;
|
||||||
|
|
||||||
// 2. Let node be the first formattable node effectively contained in the active range, or null if there is none.
|
// 2. Let node be the first formattable node effectively contained in the active range, or null if there is none.
|
||||||
GC::Ptr<DOM::Node> node;
|
auto node = first_formattable_node_effectively_contained(active_range(document));
|
||||||
auto common_ancestor = active_range->common_ancestor_container();
|
|
||||||
common_ancestor->for_each_in_subtree([&](GC::Ref<DOM::Node> descendant) {
|
|
||||||
if (is_formattable_node(descendant) && is_effectively_contained_in_range(descendant, active_range)) {
|
|
||||||
node = descendant;
|
|
||||||
return TraversalDecision::Break;
|
|
||||||
}
|
|
||||||
return TraversalDecision::Continue;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. If node is null, return overrides.
|
// 3. If node is null, return overrides.
|
||||||
if (!node)
|
if (!node)
|
||||||
|
@ -2253,44 +2310,88 @@ void remove_node_preserving_its_descendants(GC::Ref<DOM::Node> node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values
|
// https://w3c.github.io/editing/docs/execCommand/#restore-states-and-values
|
||||||
void restore_states_and_values(GC::Ref<DOM::Range>, Vector<RecordedOverride> const& overrides)
|
void restore_states_and_values(DOM::Document& document, Vector<RecordedOverride> const& overrides)
|
||||||
{
|
{
|
||||||
// FIXME: 1. Let node be the first formattable node effectively contained in the active range, or null if there is none.
|
// 1. Let node be the first formattable node effectively contained in the active range, or null if there is none.
|
||||||
|
auto node = first_formattable_node_effectively_contained(active_range(document));
|
||||||
|
|
||||||
// FIXME: 2. If node is not null, then for each (command, override) pair in overrides, in order:
|
// 2. If node is not null,
|
||||||
{
|
if (node) {
|
||||||
// FIXME: 1. If override is a boolean, and queryCommandState(command) returns something different from override, take
|
auto take_the_action_for_command = [&document](FlyString const& command, String const& value) {
|
||||||
// the action for command, with value equal to the empty string.
|
auto const& command_definition = find_command_definition(command);
|
||||||
|
// FIXME: replace with VERIFY(command_definition.has_value()) as soon as all command definitions are in place.
|
||||||
|
if (command_definition.has_value())
|
||||||
|
command_definition->action(document, value);
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME: 2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize", and
|
// then for each (command, override) pair in overrides, in order:
|
||||||
// queryCommandValue(command) returns something not equivalent to override, take the action for command, with
|
for (auto override : overrides) {
|
||||||
// value equal to override.
|
// 1. If override is a boolean, and queryCommandState(command) returns something different from override,
|
||||||
|
// take the action for command, with value equal to the empty string.
|
||||||
// FIXME: 3. Otherwise, if override is a string; and command is "createLink"; and either there is a value override for
|
if (override.value.has<bool>() && document.query_command_state(override.command) != override.value.get<bool>()) {
|
||||||
// "createLink" that is not equal to override, or there is no value override for "createLink" and node's
|
take_the_action_for_command(override.command, {});
|
||||||
// effective command value for "createLink" is not equal to override: take the action for "createLink", with
|
|
||||||
// value equal to override.
|
|
||||||
|
|
||||||
// FIXME: 4. Otherwise, if override is a string; and command is "fontSize"; and either there is a value override for
|
|
||||||
// "fontSize" that is not equal to override, or there is no value override for "fontSize" and node's
|
|
||||||
// effective command value for "fontSize" is not loosely equivalent to override:
|
|
||||||
{
|
|
||||||
// FIXME: 1. Convert override to an integer number of pixels, and set override to the legacy font size for the
|
|
||||||
// result.
|
|
||||||
|
|
||||||
// FIXME: 2. Take the action for "fontSize", with value equal to override.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: 5. Otherwise, continue this loop from the beginning.
|
// 2. Otherwise, if override is a string, and command is neither "createLink" nor "fontSize", and
|
||||||
|
// queryCommandValue(command) returns something not equivalent to override, take the action for command,
|
||||||
|
// with value equal to override.
|
||||||
|
else if (override.value.has<String>() && !override.command.is_one_of(CommandNames::createLink, CommandNames::fontSize)
|
||||||
|
&& document.query_command_value(override.command) != override.value.get<String>()) {
|
||||||
|
take_the_action_for_command(override.command, override.value.get<String>());
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: 6. Set node to the first formattable node effectively contained in the active range, if there is one.
|
// 3. Otherwise, if override is a string; and command is "createLink"; and either there is a value override
|
||||||
|
// for "createLink" that is not equal to override, or there is no value override for "createLink" and
|
||||||
|
// node's effective command value for "createLink" is not equal to override: take the action for
|
||||||
|
// "createLink", with value equal to override.
|
||||||
|
else if (auto value_override = document.command_value_override(CommandNames::createLink);
|
||||||
|
override.value.has<String>() && override.command == CommandNames::createLink
|
||||||
|
&& ((value_override.has_value() && value_override.value() != override.value.get<String>())
|
||||||
|
|| (!value_override.has_value()
|
||||||
|
&& effective_command_value(node, CommandNames::createLink) != override.value.get<String>()))) {
|
||||||
|
take_the_action_for_command(CommandNames::createLink, override.value.get<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Otherwise, if override is a string; and command is "fontSize"; and either there is a value override
|
||||||
|
// for "fontSize" that is not equal to override, or there is no value override for "fontSize" and node's
|
||||||
|
// effective command value for "fontSize" is not loosely equivalent to override:
|
||||||
|
else if (auto value_override = document.command_value_override(CommandNames::fontSize);
|
||||||
|
override.value.has<String>() && override.command == CommandNames::fontSize
|
||||||
|
&& ((value_override.has_value() && value_override.value() != override.value.get<String>())
|
||||||
|
|| (!value_override.has_value()
|
||||||
|
&& !values_are_loosely_equivalent(
|
||||||
|
CommandNames::fontSize,
|
||||||
|
effective_command_value(node, CommandNames::fontSize),
|
||||||
|
override.value.get<String>())))) {
|
||||||
|
// 1. Convert override to an integer number of pixels, and set override to the legacy font size for the
|
||||||
|
// result.
|
||||||
|
auto override_pixel_size = font_size_to_pixel_size(override.value.get<String>());
|
||||||
|
override.value = legacy_font_size(override_pixel_size.to_int());
|
||||||
|
|
||||||
|
// 2. Take the action for "fontSize", with value equal to override.
|
||||||
|
take_the_action_for_command(CommandNames::fontSize, override.value.get<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Otherwise, continue this loop from the beginning.
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Set node to the first formattable node effectively contained in the active range, if there is one.
|
||||||
|
if (auto new_formattable_node = first_formattable_node_effectively_contained(active_range(document)))
|
||||||
|
node = new_formattable_node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Otherwise, for each (command, override) pair in overrides, in order:
|
// 3. Otherwise, for each (command, override) pair in overrides, in order:
|
||||||
for ([[maybe_unused]] auto const& override : overrides) {
|
else {
|
||||||
// FIXME: 1. If override is a boolean, set the state override for command to override.
|
for (auto const& override : overrides) {
|
||||||
|
// 1. If override is a boolean, set the state override for command to override.
|
||||||
// FIXME: 2. If override is a string, set the value override for command to override.
|
// 2. If override is a string, set the value override for command to override.
|
||||||
|
override.value.visit(
|
||||||
|
[&](bool value) { document.set_command_state_override(override.command, value); },
|
||||||
|
[&](String const& value) { document.set_command_value_override(override.command, value); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2564,6 +2665,72 @@ void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const& node_list)
|
||||||
remove_extraneous_line_breaks_at_the_end_of_node(*last_node->parent());
|
remove_extraneous_line_breaks_at_the_end_of_node(*last_node->parent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#equivalent-values
|
||||||
|
bool values_are_equivalent(FlyString const& command, Optional<String> a, Optional<String> b)
|
||||||
|
{
|
||||||
|
// Two quantities are equivalent values for a command if either both are null,
|
||||||
|
if (!a.has_value() && !b.has_value())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// NOTE: Both need to be strings for all remaining conditions.
|
||||||
|
if (!a.has_value() || !b.has_value())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// or both are strings and the command defines equivalent values and they match the definition.
|
||||||
|
if (command.is_one_of(CommandNames::backColor, CommandNames::foreColor, CommandNames::hiliteColor)) {
|
||||||
|
// Either both strings are valid CSS colors and have the same red, green, blue, and alpha components, or neither
|
||||||
|
// string is a valid CSS color.
|
||||||
|
auto a_color = Color::from_string(a.value());
|
||||||
|
auto b_color = Color::from_string(b.value());
|
||||||
|
if (a_color.has_value())
|
||||||
|
return a_color == b_color;
|
||||||
|
return !a_color.has_value() && !b_color.has_value();
|
||||||
|
}
|
||||||
|
if (command == CommandNames::bold) {
|
||||||
|
// Either the two strings are equal, or one is "bold" and the other is "700", or one is "normal" and the other
|
||||||
|
// is "400".
|
||||||
|
if (a.value() == b.value())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
auto either_is_bold = first_is_one_of("bold"sv, a.value(), b.value());
|
||||||
|
auto either_is_700 = first_is_one_of("700"sv, a.value(), b.value());
|
||||||
|
auto either_is_normal = first_is_one_of("normal"sv, a.value(), b.value());
|
||||||
|
auto either_is_400 = first_is_one_of("400"sv, a.value(), b.value());
|
||||||
|
|
||||||
|
return (either_is_bold && either_is_700) || (either_is_normal && either_is_400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// or both are strings and they're equal and the command does not define any equivalent values,
|
||||||
|
return a.value() == b.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://w3c.github.io/editing/docs/execCommand/#loosely-equivalent-values
|
||||||
|
bool values_are_loosely_equivalent(FlyString const& command, Optional<String> a, Optional<String> b)
|
||||||
|
{
|
||||||
|
// Two quantities are loosely equivalent values for a command if either they are equivalent values for the command,
|
||||||
|
if (values_are_equivalent(command, a, b))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// or if the command is the fontSize command; one of the quantities is one of "x-small", "small", "medium", "large",
|
||||||
|
// "x-large", "xx-large", or "xxx-large"; and the other quantity is the resolved value of "font-size" on a font
|
||||||
|
// element whose size attribute has the corresponding value set ("1" through "7" respectively).
|
||||||
|
if (command == CommandNames::fontSize && a.has_value() && b.has_value()) {
|
||||||
|
static constexpr Array named_quantities { "x-small"sv, "small"sv, "medium"sv, "large"sv, "x-large"sv,
|
||||||
|
"xx-large"sv, "xxx-large"sv };
|
||||||
|
static constexpr Array size_quantities { "1"sv, "2"sv, "3"sv, "4"sv, "5"sv, "6"sv, "7"sv };
|
||||||
|
static_assert(named_quantities.size() == size_quantities.size());
|
||||||
|
|
||||||
|
auto a_index = named_quantities.first_index_of(a.value())
|
||||||
|
.value_or_lazy_evaluated_optional([&] { return size_quantities.first_index_of(a.value()); });
|
||||||
|
auto b_index = named_quantities.first_index_of(b.value())
|
||||||
|
.value_or_lazy_evaluated_optional([&] { return size_quantities.first_index_of(b.value()); });
|
||||||
|
|
||||||
|
return a_index.has_value() && a_index == b_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// https://w3c.github.io/editing/docs/execCommand/#wrap
|
// https://w3c.github.io/editing/docs/execCommand/#wrap
|
||||||
GC::Ptr<DOM::Node> wrap(
|
GC::Ptr<DOM::Node> wrap(
|
||||||
Vector<GC::Ref<DOM::Node>> node_list,
|
Vector<GC::Ref<DOM::Node>> node_list,
|
||||||
|
@ -2745,6 +2912,38 @@ GC::Ptr<DOM::Node> wrap(
|
||||||
return new_parent;
|
return new_parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GC::Ptr<DOM::Node> first_formattable_node_effectively_contained(GC::Ptr<DOM::Range> range)
|
||||||
|
{
|
||||||
|
GC::Ptr<DOM::Node> node;
|
||||||
|
for_each_node_effectively_contained_in_range(range, [&](GC::Ref<DOM::Node> descendant) {
|
||||||
|
if (is_formattable_node(descendant)) {
|
||||||
|
node = descendant;
|
||||||
|
return TraversalDecision::Break;
|
||||||
|
}
|
||||||
|
return TraversalDecision::Continue;
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
CSSPixels font_size_to_pixel_size(StringView font_size)
|
||||||
|
{
|
||||||
|
auto pixel_size = CSS::StyleComputer::default_user_font_size();
|
||||||
|
|
||||||
|
// Try to map the font size directly to a keyword (e.g. medium or x-large)
|
||||||
|
auto keyword = CSS::keyword_from_string(font_size);
|
||||||
|
|
||||||
|
// If that failed, try to interpret it as a legacy font size (e.g. 1 through 7)
|
||||||
|
if (!keyword.has_value())
|
||||||
|
keyword = HTML::HTMLFontElement::parse_legacy_font_size(font_size);
|
||||||
|
|
||||||
|
// If that also failed, give up
|
||||||
|
if (!keyword.has_value())
|
||||||
|
return pixel_size;
|
||||||
|
|
||||||
|
// Return scaled pixel size
|
||||||
|
return pixel_size * CSS::StyleComputer::absolute_size_mapping(keyword.release_value());
|
||||||
|
}
|
||||||
|
|
||||||
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range> range, Function<TraversalDecision(GC::Ref<DOM::Node>)> callback)
|
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range> range, Function<TraversalDecision(GC::Ref<DOM::Node>)> callback)
|
||||||
{
|
{
|
||||||
if (!range)
|
if (!range)
|
||||||
|
|
|
@ -66,26 +66,32 @@ bool is_single_line_container(GC::Ref<DOM::Node>);
|
||||||
bool is_visible_node(GC::Ref<DOM::Node>);
|
bool is_visible_node(GC::Ref<DOM::Node>);
|
||||||
bool is_whitespace_node(GC::Ref<DOM::Node>);
|
bool is_whitespace_node(GC::Ref<DOM::Node>);
|
||||||
DOM::BoundaryPoint last_equivalent_point(DOM::BoundaryPoint);
|
DOM::BoundaryPoint last_equivalent_point(DOM::BoundaryPoint);
|
||||||
|
String legacy_font_size(int);
|
||||||
void move_node_preserving_ranges(GC::Ref<DOM::Node>, GC::Ref<DOM::Node> new_parent, u32 new_index);
|
void move_node_preserving_ranges(GC::Ref<DOM::Node>, GC::Ref<DOM::Node> new_parent, u32 new_index);
|
||||||
Optional<DOM::BoundaryPoint> next_equivalent_point(DOM::BoundaryPoint);
|
Optional<DOM::BoundaryPoint> next_equivalent_point(DOM::BoundaryPoint);
|
||||||
void normalize_sublists_in_node(GC::Ref<DOM::Node>);
|
void normalize_sublists_in_node(GC::Ref<DOM::Node>);
|
||||||
bool precedes_a_line_break(GC::Ref<DOM::Node>);
|
bool precedes_a_line_break(GC::Ref<DOM::Node>);
|
||||||
Optional<DOM::BoundaryPoint> previous_equivalent_point(DOM::BoundaryPoint);
|
Optional<DOM::BoundaryPoint> previous_equivalent_point(DOM::BoundaryPoint);
|
||||||
Vector<RecordedOverride> record_current_states_and_values(GC::Ref<DOM::Range>);
|
Vector<RecordedOverride> record_current_overrides(DOM::Document const&);
|
||||||
|
Vector<RecordedOverride> record_current_states_and_values(DOM::Document const&);
|
||||||
Vector<RecordedNodeValue> record_the_values_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
|
Vector<RecordedNodeValue> record_the_values_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
|
||||||
void remove_extraneous_line_breaks_at_the_end_of_node(GC::Ref<DOM::Node>);
|
void remove_extraneous_line_breaks_at_the_end_of_node(GC::Ref<DOM::Node>);
|
||||||
void remove_extraneous_line_breaks_before_node(GC::Ref<DOM::Node>);
|
void remove_extraneous_line_breaks_before_node(GC::Ref<DOM::Node>);
|
||||||
void remove_extraneous_line_breaks_from_a_node(GC::Ref<DOM::Node>);
|
void remove_extraneous_line_breaks_from_a_node(GC::Ref<DOM::Node>);
|
||||||
void remove_node_preserving_its_descendants(GC::Ref<DOM::Node>);
|
void remove_node_preserving_its_descendants(GC::Ref<DOM::Node>);
|
||||||
void restore_states_and_values(GC::Ref<DOM::Range>, Vector<RecordedOverride> const&);
|
void restore_states_and_values(DOM::Document&, Vector<RecordedOverride> const&);
|
||||||
void restore_the_values_of_nodes(Vector<RecordedNodeValue> const&);
|
void restore_the_values_of_nodes(Vector<RecordedNodeValue> const&);
|
||||||
GC::Ref<DOM::Element> set_the_tag_name(GC::Ref<DOM::Element>, FlyString const&);
|
GC::Ref<DOM::Element> set_the_tag_name(GC::Ref<DOM::Element>, FlyString const&);
|
||||||
Optional<String> specified_command_value(GC::Ref<DOM::Element>, FlyString const& command);
|
Optional<String> specified_command_value(GC::Ref<DOM::Element>, FlyString const& command);
|
||||||
void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
|
void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const&);
|
||||||
|
bool values_are_equivalent(FlyString const&, Optional<String>, Optional<String>);
|
||||||
|
bool values_are_loosely_equivalent(FlyString const&, Optional<String>, Optional<String>);
|
||||||
GC::Ptr<DOM::Node> wrap(Vector<GC::Ref<DOM::Node>>, Function<bool(GC::Ref<DOM::Node>)> sibling_criteria, Function<GC::Ptr<DOM::Node>()> new_parent_instructions);
|
GC::Ptr<DOM::Node> wrap(Vector<GC::Ref<DOM::Node>>, Function<bool(GC::Ref<DOM::Node>)> sibling_criteria, Function<GC::Ptr<DOM::Node>()> new_parent_instructions);
|
||||||
|
|
||||||
// Utility methods:
|
// Utility methods:
|
||||||
|
|
||||||
|
GC::Ptr<DOM::Node> first_formattable_node_effectively_contained(GC::Ptr<DOM::Range>);
|
||||||
|
CSSPixels font_size_to_pixel_size(StringView);
|
||||||
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range>, Function<TraversalDecision(GC::Ref<DOM::Node>)>);
|
void for_each_node_effectively_contained_in_range(GC::Ptr<DOM::Range>, Function<TraversalDecision(GC::Ref<DOM::Node>)>);
|
||||||
bool has_visible_children(GC::Ref<DOM::Node>);
|
bool has_visible_children(GC::Ref<DOM::Node>);
|
||||||
bool is_heading(FlyString const&);
|
bool is_heading(FlyString const&);
|
||||||
|
|
|
@ -26,7 +26,7 @@ enum class Mode {
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/rendering.html#rules-for-parsing-a-legacy-font-size
|
// https://html.spec.whatwg.org/multipage/rendering.html#rules-for-parsing-a-legacy-font-size
|
||||||
static Optional<CSS::Keyword> parse_legacy_font_size(StringView string)
|
Optional<CSS::Keyword> HTMLFontElement::parse_legacy_font_size(StringView string)
|
||||||
{
|
{
|
||||||
// 1. Let input be the attribute's value.
|
// 1. Let input be the attribute's value.
|
||||||
// 2. Let position be a pointer into input, initially pointing at the start of the string.
|
// 2. Let position be a pointer into input, initially pointing at the start of the string.
|
||||||
|
|
|
@ -20,6 +20,8 @@ public:
|
||||||
virtual bool is_presentational_hint(FlyString const&) const override;
|
virtual bool is_presentational_hint(FlyString const&) const override;
|
||||||
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
|
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
|
||||||
|
|
||||||
|
static Optional<CSS::Keyword> parse_legacy_font_size(StringView);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HTMLFontElement(DOM::Document&, DOM::QualifiedName);
|
HTMLFontElement(DOM::Document&, DOM::QualifiedName);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue