mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-04 16:11:54 +00:00
LibWeb: Implement document.execCommand("insertParagraph")
This commit is contained in:
parent
2e29d3fb57
commit
4f76cec096
Notes:
github-actions[bot]
2024-12-04 05:53:10 +00:00
Author: https://github.com/gmta
Commit: 4f76cec096
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2735
Reviewed-by: https://github.com/tcl3
6 changed files with 668 additions and 9 deletions
|
@ -24,6 +24,98 @@
|
|||
|
||||
namespace Web::Editing {
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#block-extend
|
||||
GC::Ref<DOM::Range> block_extend_a_range(DOM::Range& range)
|
||||
{
|
||||
// 1. Let start node, start offset, end node, and end offset be the start and end nodes and offsets of range.
|
||||
GC::Ptr<DOM::Node> start_node = range.start_container();
|
||||
auto start_offset = range.start_offset();
|
||||
GC::Ptr<DOM::Node> end_node = range.end_container();
|
||||
auto end_offset = range.end_offset();
|
||||
|
||||
// 2. If some inclusive ancestor of start node is an li, set start offset to the index of the last such li in tree
|
||||
// order, and set start node to that li's parent.
|
||||
auto ancestor = start_node;
|
||||
do {
|
||||
if (is<HTML::HTMLLIElement>(*ancestor)) {
|
||||
start_offset = ancestor->index();
|
||||
start_node = ancestor->parent();
|
||||
break;
|
||||
}
|
||||
ancestor = ancestor->parent();
|
||||
} while (ancestor);
|
||||
|
||||
// 3. If (start node, start offset) is not a block start point, repeat the following steps:
|
||||
if (!is_block_start_point(*start_node, start_offset)) {
|
||||
do {
|
||||
// 1. If start offset is zero, set it to start node's index, then set start node to its parent.
|
||||
if (start_offset == 0) {
|
||||
start_offset = start_node->index();
|
||||
start_node = start_node->parent();
|
||||
}
|
||||
|
||||
// 2. Otherwise, subtract one from start offset.
|
||||
else {
|
||||
--start_offset;
|
||||
}
|
||||
|
||||
// 3. If (start node, start offset) is a block boundary point, break from this loop.
|
||||
} while (!is_block_boundary_point(*start_node, start_offset));
|
||||
}
|
||||
|
||||
// 4. While start offset is zero and start node's parent is not null, set start offset to start node's index, then
|
||||
// set start node to its parent.
|
||||
while (start_offset == 0 && start_node->parent()) {
|
||||
start_offset = start_node->index();
|
||||
start_node = start_node->parent();
|
||||
}
|
||||
|
||||
// 5. If some inclusive ancestor of end node is an li, set end offset to one plus the index of the last such li in
|
||||
// tree order, and set end node to that li's parent.
|
||||
ancestor = end_node;
|
||||
do {
|
||||
if (is<HTML::HTMLLIElement>(*ancestor)) {
|
||||
end_offset = ancestor->index() + 1;
|
||||
end_node = ancestor->parent();
|
||||
break;
|
||||
}
|
||||
ancestor = ancestor->parent();
|
||||
} while (ancestor);
|
||||
|
||||
// 6. If (end node, end offset) is not a block end point, repeat the following steps:
|
||||
if (!is_block_end_point(*end_node, end_offset)) {
|
||||
do {
|
||||
// 1. If end offset is end node's length, set it to one plus end node's index, then set end node to its
|
||||
// parent.
|
||||
if (end_offset == end_node->length()) {
|
||||
end_offset = end_node->index() + 1;
|
||||
end_node = end_node->parent();
|
||||
}
|
||||
|
||||
// 2. Otherwise, add one to end offset.
|
||||
else {
|
||||
++end_offset;
|
||||
}
|
||||
|
||||
// 3. If (end node, end offset) is a block boundary point, break from this loop.
|
||||
} while (!is_block_boundary_point(*end_node, end_offset));
|
||||
}
|
||||
|
||||
// 7. While end offset is end node's length and end node's parent is not null, set end offset to one plus end node's
|
||||
// index, then set end node to its parent.
|
||||
while (end_offset == end_node->length() && end_node->parent()) {
|
||||
end_offset = end_node->index() + 1;
|
||||
end_node = end_node->parent();
|
||||
}
|
||||
|
||||
// 8. Let new range be a new range whose start and end nodes and offsets are start node, start offset, end node, and
|
||||
// end offset.
|
||||
auto new_range = DOM::Range::create(*start_node, start_offset, *end_node, end_offset);
|
||||
|
||||
// 9. Return new range.
|
||||
return new_range;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#canonical-space-sequence
|
||||
String canonical_space_sequence(u32 length, bool non_breaking_start, bool non_breaking_end)
|
||||
{
|
||||
|
@ -521,15 +613,6 @@ bool is_allowed_child_of_node(Variant<GC::Ref<DOM::Node>, FlyString> child, Vari
|
|||
auto child_local_name = child.get<FlyString>();
|
||||
|
||||
// 6. If parent is an HTML element:
|
||||
auto is_heading = [](FlyString const& local_name) {
|
||||
return local_name.is_one_of(
|
||||
HTML::TagNames::h1,
|
||||
HTML::TagNames::h2,
|
||||
HTML::TagNames::h3,
|
||||
HTML::TagNames::h4,
|
||||
HTML::TagNames::h5,
|
||||
HTML::TagNames::h6);
|
||||
};
|
||||
if (is<HTML::HTMLElement>(parent_node.ptr())) {
|
||||
auto& parent_html_element = static_cast<HTML::HTMLElement&>(*parent.get<GC::Ref<DOM::Node>>());
|
||||
|
||||
|
@ -932,6 +1015,19 @@ bool is_name_of_an_element_with_inline_contents(FlyString const& local_name)
|
|||
HTML::TagNames::tt);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#non-list-single-line-container
|
||||
bool is_non_list_single_line_container(GC::Ref<DOM::Node> node)
|
||||
{
|
||||
// A non-list single-line container is an HTML element with local name "address", "divis_", "h1", "h2", "h3", "h4",
|
||||
// "h5", "h6", "listing", "p", "pre", or "xmp".
|
||||
if (!is<HTML::HTMLElement>(*node))
|
||||
return false;
|
||||
auto& local_name = static_cast<HTML::HTMLElement&>(*node).local_name();
|
||||
return is_heading(local_name)
|
||||
|| local_name.is_one_of(HTML::TagNames::address, HTML::TagNames::div, HTML::TagNames::listing,
|
||||
HTML::TagNames::p, HTML::TagNames::pre, HTML::TagNames::xmp);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#prohibited-paragraph-child
|
||||
bool is_prohibited_paragraph_child(GC::Ref<DOM::Node> node)
|
||||
{
|
||||
|
@ -996,6 +1092,19 @@ bool is_prohibited_paragraph_child_name(FlyString const& local_name)
|
|||
HTML::TagNames::xmp);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#single-line-container
|
||||
bool is_single_line_container(GC::Ref<DOM::Node> node)
|
||||
{
|
||||
// A single-line container is either a non-list single-line container, or an HTML element with local name "li",
|
||||
// "dt", or "dd".
|
||||
if (is_non_list_single_line_container(node))
|
||||
return true;
|
||||
if (!is<HTML::HTMLElement>(*node))
|
||||
return false;
|
||||
auto& html_element = static_cast<HTML::HTMLElement&>(*node);
|
||||
return html_element.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd);
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#visible
|
||||
bool is_visible_node(GC::Ref<DOM::Node> node)
|
||||
{
|
||||
|
@ -1571,4 +1680,196 @@ void split_the_parent_of_nodes(Vector<GC::Ref<DOM::Node>> const& nodes)
|
|||
remove_extraneous_line_breaks_at_the_end_of_node(*last_node->parent());
|
||||
}
|
||||
|
||||
// https://w3c.github.io/editing/docs/execCommand/#wrap
|
||||
GC::Ptr<DOM::Node> wrap(
|
||||
Vector<GC::Ref<DOM::Node>> node_list,
|
||||
Function<bool(GC::Ref<DOM::Node>)> sibling_criteria,
|
||||
Function<GC::Ptr<DOM::Node>()> new_parent_instructions)
|
||||
{
|
||||
VERIFY(!node_list.is_empty());
|
||||
|
||||
// If not provided, sibling criteria returns false and new parent instructions returns null.
|
||||
if (!sibling_criteria)
|
||||
sibling_criteria = [](auto) { return false; };
|
||||
if (!new_parent_instructions)
|
||||
new_parent_instructions = [] { return nullptr; };
|
||||
|
||||
// 1. If every member of node list is invisible, and none is a br, return null and abort these steps.
|
||||
auto any_node_visible_or_br = false;
|
||||
for (auto& node : node_list) {
|
||||
if (is_visible_node(node) || is<HTML::HTMLBRElement>(*node)) {
|
||||
any_node_visible_or_br = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_node_visible_or_br)
|
||||
return {};
|
||||
|
||||
// 2. If node list's first member's parent is null, return null and abort these steps.
|
||||
if (!node_list.first()->parent())
|
||||
return {};
|
||||
|
||||
// 3. If node list's last member is an inline node that's not a br, and node list's last member's nextSibling is a
|
||||
// br, append that br to node list.
|
||||
auto last_member = node_list.last();
|
||||
if (is_inline_node(last_member) && !is<HTML::HTMLBRElement>(*last_member) && is<HTML::HTMLBRElement>(last_member->next_sibling()))
|
||||
node_list.append(*last_member->next_sibling());
|
||||
|
||||
// 4. While node list's first member's previousSibling is invisible, prepend it to node list.
|
||||
while (node_list.first()->previous_sibling() && is_invisible_node(*node_list.first()->previous_sibling()))
|
||||
node_list.prepend(*node_list.first()->previous_sibling());
|
||||
|
||||
// 5. While node list's last member's nextSibling is invisible, append it to node list.
|
||||
while (node_list.last()->next_sibling() && is_invisible_node(*node_list.last()->next_sibling()))
|
||||
node_list.append(*node_list.last()->next_sibling());
|
||||
|
||||
auto new_parent = [&]() -> GC::Ptr<DOM::Node> {
|
||||
// 6. If the previousSibling of the first member of node list is editable and running sibling criteria on it returns
|
||||
// true, let new parent be the previousSibling of the first member of node list.
|
||||
GC::Ptr<DOM::Node> previous_sibling = node_list.first()->previous_sibling();
|
||||
if (previous_sibling && previous_sibling->is_editable() && sibling_criteria(*previous_sibling))
|
||||
return previous_sibling;
|
||||
|
||||
// 7. Otherwise, if the nextSibling of the last member of node list is editable and running sibling criteria on it
|
||||
// returns true, let new parent be the nextSibling of the last member of node list.
|
||||
GC::Ptr<DOM::Node> next_sibling = node_list.last()->next_sibling();
|
||||
if (next_sibling && next_sibling->is_editable() && sibling_criteria(*next_sibling))
|
||||
return next_sibling;
|
||||
|
||||
// 8. Otherwise, run new parent instructions, and let new parent be the result.
|
||||
return new_parent_instructions();
|
||||
}();
|
||||
|
||||
// 9. If new parent is null, abort these steps and return null.
|
||||
if (!new_parent)
|
||||
return {};
|
||||
|
||||
// 10. If new parent's parent is null:
|
||||
if (!new_parent->parent()) {
|
||||
// 1. Insert new parent into the parent of the first member of node list immediately before the first member of
|
||||
// node list.
|
||||
auto first_member = node_list.first();
|
||||
first_member->parent()->insert_before(*new_parent, first_member);
|
||||
|
||||
// FIXME: 2. If any range has a boundary point with node equal to the parent of new parent and offset equal to the
|
||||
// index of new parent, add one to that boundary point's offset.
|
||||
}
|
||||
|
||||
// 11. Let original parent be the parent of the first member of node list.
|
||||
auto const original_parent = GC::Ptr { node_list.first()->parent() };
|
||||
|
||||
// 12. If new parent is before the first member of node list in tree order:
|
||||
if (new_parent->is_before(node_list.first())) {
|
||||
// 1. If new parent is not an inline node, but the last visible child of new parent and the first visible member
|
||||
// of node list are both inline nodes, and the last child of new parent is not a br, call createElement("br")
|
||||
// on the ownerDocument of new parent and append the result as the last child of new parent.
|
||||
if (!is_inline_node(*new_parent)) {
|
||||
auto last_visible_child = [&] -> GC::Ref<DOM::Node> {
|
||||
GC::Ptr<DOM::Node> child = new_parent->last_child();
|
||||
do {
|
||||
if (is_visible_node(*child))
|
||||
return *child;
|
||||
child = child->previous_sibling();
|
||||
} while (child);
|
||||
VERIFY_NOT_REACHED();
|
||||
}();
|
||||
auto first_visible_member = [&] -> GC::Ref<DOM::Node> {
|
||||
for (auto& member : node_list) {
|
||||
if (is_visible_node(member))
|
||||
return member;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}();
|
||||
if (is_inline_node(last_visible_child) && is_inline_node(first_visible_member)
|
||||
&& !is<HTML::HTMLBRElement>(new_parent->last_child())) {
|
||||
auto br_element = MUST(DOM::create_element(*new_parent->owner_document(), HTML::TagNames::br, Namespace::HTML));
|
||||
MUST(new_parent->append_child(br_element));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. For each node in node list, append node as the last child of new parent, preserving ranges.
|
||||
auto new_position = new_parent->child_count();
|
||||
for (auto& node : node_list)
|
||||
move_node_preserving_ranges(node, *new_parent, new_position++);
|
||||
}
|
||||
|
||||
// 13. Otherwise:
|
||||
else {
|
||||
// 1. If new parent is not an inline node, but the first visible child of new parent and the last visible member
|
||||
// of node list are both inline nodes, and the last member of node list is not a br, call createElement("br")
|
||||
// on the ownerDocument of new parent and insert the result as the first child of new parent.
|
||||
if (!is_inline_node(*new_parent)) {
|
||||
auto first_visible_child = [&] -> GC::Ref<DOM::Node> {
|
||||
GC::Ptr<DOM::Node> child = new_parent->first_child();
|
||||
do {
|
||||
if (is_visible_node(*child))
|
||||
return *child;
|
||||
child = child->next_sibling();
|
||||
} while (child);
|
||||
VERIFY_NOT_REACHED();
|
||||
}();
|
||||
auto last_visible_member = [&] -> GC::Ref<DOM::Node> {
|
||||
for (auto& member : node_list.in_reverse()) {
|
||||
if (is_visible_node(member))
|
||||
return member;
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}();
|
||||
if (is_inline_node(first_visible_child) && is_inline_node(last_visible_member)
|
||||
&& !is<HTML::HTMLBRElement>(*node_list.last())) {
|
||||
auto br_element = MUST(DOM::create_element(*new_parent->owner_document(), HTML::TagNames::br, Namespace::HTML));
|
||||
new_parent->insert_before(br_element, new_parent->first_child());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. For each node in node list, in reverse order, insert node as the first child of new parent, preserving
|
||||
// ranges.
|
||||
for (auto& node : node_list.in_reverse())
|
||||
move_node_preserving_ranges(node, *new_parent, 0);
|
||||
}
|
||||
|
||||
// 14. If original parent is editable and has no children, remove it from its parent.
|
||||
if (original_parent->is_editable() && !original_parent->has_children())
|
||||
original_parent->remove();
|
||||
|
||||
// 15. If new parent's nextSibling is editable and running sibling criteria on it returns true:
|
||||
GC::Ptr<DOM::Node> next_sibling = new_parent->next_sibling();
|
||||
if (next_sibling && next_sibling->is_editable() && sibling_criteria(*next_sibling)) {
|
||||
// 1. If new parent is not an inline node, but new parent's last child and new parent's nextSibling's first
|
||||
// child are both inline nodes, and new parent's last child is not a br, call createElement("br") on the
|
||||
// ownerDocument of new parent and append the result as the last child of new parent.
|
||||
if (!is_inline_node(*new_parent) && is_inline_node(*new_parent->last_child())
|
||||
&& is_inline_node(*next_sibling->first_child()) && !is<HTML::HTMLBRElement>(new_parent->last_child())) {
|
||||
auto br_element = MUST(DOM::create_element(*new_parent->owner_document(), HTML::TagNames::br, Namespace::HTML));
|
||||
MUST(new_parent->append_child(br_element));
|
||||
}
|
||||
|
||||
// 2. While new parent's nextSibling has children, append its first child as the last child of new parent,
|
||||
// preserving ranges.
|
||||
auto new_position = new_parent->child_count();
|
||||
while (next_sibling->has_children())
|
||||
move_node_preserving_ranges(*next_sibling->first_child(), *new_parent, new_position++);
|
||||
|
||||
// 3. Remove new parent's nextSibling from its parent.
|
||||
next_sibling->remove();
|
||||
}
|
||||
|
||||
// 16. Remove extraneous line breaks from new parent.
|
||||
remove_extraneous_line_breaks_from_a_node(*new_parent);
|
||||
|
||||
// 17. Return new parent.
|
||||
return new_parent;
|
||||
}
|
||||
|
||||
bool is_heading(FlyString const& local_name)
|
||||
{
|
||||
return local_name.is_one_of(
|
||||
HTML::TagNames::h1,
|
||||
HTML::TagNames::h2,
|
||||
HTML::TagNames::h3,
|
||||
HTML::TagNames::h4,
|
||||
HTML::TagNames::h5,
|
||||
HTML::TagNames::h6);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue