diff --git a/Tests/LibWeb/Text/expected/DOM/Node-normalize.txt b/Tests/LibWeb/Text/expected/DOM/Node-normalize.txt new file mode 100644 index 00000000000..008baa6bb85 --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/Node-normalize.txt @@ -0,0 +1,8 @@ +Document fragment initial text: 12, child nodes: 3 +Element initial text: 34, child nodes: 2 +Element text after document.normalize(): 34, child nodes: 2 +Document fragment text after documentFragment.normalize(): 1234, child nodes: 2 +Text node 1 data: 12 +Text node 2 data: 2 +Text node 3 data: 34 +Text node 4 data: 4 \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/DOM/Node-normalize.html b/Tests/LibWeb/Text/input/DOM/Node-normalize.html new file mode 100644 index 00000000000..46a5847fb5e --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/Node-normalize.html @@ -0,0 +1,29 @@ + + + diff --git a/Userland/Libraries/LibWeb/DOM/Node.cpp b/Userland/Libraries/LibWeb/DOM/Node.cpp index dc5e32b47fd..ab966e0a792 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.cpp +++ b/Userland/Libraries/LibWeb/DOM/Node.cpp @@ -212,6 +212,114 @@ void Node::set_text_content(Optional const& maybe_content) document().bump_dom_tree_version(); } +// https://dom.spec.whatwg.org/#dom-node-normalize +WebIDL::ExceptionOr Node::normalize() +{ + auto contiguous_exclusive_text_nodes_excluding_self = [](Node& node) { + // https://dom.spec.whatwg.org/#contiguous-exclusive-text-nodes + // The contiguous exclusive Text nodes of a node node are node, node’s previous sibling exclusive Text node, if any, + // and its contiguous exclusive Text nodes, and node’s next sibling exclusive Text node, if any, + // and its contiguous exclusive Text nodes, avoiding any duplicates. + // NOTE: The callers of this method require node itself to be excluded. + Vector nodes; + + auto* current_node = node.previous_sibling(); + while (current_node) { + if (!current_node->is_text()) + break; + + nodes.append(static_cast(current_node)); + current_node = current_node->previous_sibling(); + } + + // Reverse the order of the nodes so that they are in tree order. + nodes.reverse(); + + current_node = node.next_sibling(); + while (current_node) { + if (!current_node->is_text()) + break; + + nodes.append(static_cast(current_node)); + current_node = current_node->next_sibling(); + } + + return nodes; + }; + + // The normalize() method steps are to run these steps for each descendant exclusive Text node node of this + Vector descendant_exclusive_text_nodes; + for_each_in_inclusive_subtree_of_type([&](Text const& node) { + if (!node.is_cdata_section()) + descendant_exclusive_text_nodes.append(const_cast(node)); + + return TraversalDecision::Continue; + }); + + for (auto& node : descendant_exclusive_text_nodes) { + // 1. Let length be node’s length. + auto& character_data = static_cast(node); + auto length = character_data.length_in_utf16_code_units(); + + // 2. If length is zero, then remove node and continue with the next exclusive Text node, if any. + if (length == 0) { + if (node.parent()) + node.remove(); + continue; + } + + // 3. Let data be the concatenation of the data of node’s contiguous exclusive Text nodes (excluding itself), in tree order. + StringBuilder data; + for (auto const& text_node : contiguous_exclusive_text_nodes_excluding_self(node)) + data.append(text_node->data()); + + // 4. Replace data with node node, offset length, count 0, and data data. + TRY(character_data.replace_data(length, 0, MUST(data.to_string()))); + + // 5. Let currentNode be node’s next sibling. + auto* current_node = node.next_sibling(); + + // 6. While currentNode is an exclusive Text node: + while (current_node && is(*current_node)) { + // 1. For each live range whose start node is currentNode, add length to its start offset and set its start node to node. + for (auto& range : Range::live_ranges()) { + if (range->start_container() == current_node) + TRY(range->set_start(node, range->start_offset() + length)); + } + + // 2. For each live range whose end node is currentNode, add length to its end offset and set its end node to node. + for (auto& range : Range::live_ranges()) { + if (range->end_container() == current_node) + TRY(range->set_end(node, range->end_offset() + length)); + } + + // 3. For each live range whose start node is currentNode’s parent and start offset is currentNode’s index, set its start node to node and its start offset to length. + for (auto& range : Range::live_ranges()) { + if (range->start_container() == current_node->parent() && range->start_offset() == current_node->index()) + TRY(range->set_start(node, length)); + } + + // 4. For each live range whose end node is currentNode’s parent and end offset is currentNode’s index, set its end node to node and its end offset to length. + for (auto& range : Range::live_ranges()) { + if (range->end_container() == current_node->parent() && range->end_offset() == current_node->index()) + TRY(range->set_end(node, length)); + } + + // 5. Add currentNode’s length to length. + length += static_cast(*current_node).length(); + + // 6. Set currentNode to its next sibling. + current_node = current_node->next_sibling(); + } + + // 7. Remove node’s contiguous exclusive Text nodes (excluding itself), in tree order. + for (auto const& text_node : contiguous_exclusive_text_nodes_excluding_self(node)) + text_node->remove(); + } + + return {}; +} + // https://dom.spec.whatwg.org/#dom-node-nodevalue Optional Node::node_value() const { diff --git a/Userland/Libraries/LibWeb/DOM/Node.h b/Userland/Libraries/LibWeb/DOM/Node.h index fafc8e760a6..14fd18c2ee0 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.h +++ b/Userland/Libraries/LibWeb/DOM/Node.h @@ -156,6 +156,8 @@ public: Optional text_content() const; void set_text_content(Optional const&); + WebIDL::ExceptionOr normalize(); + Optional node_value() const; void set_node_value(Optional const&); diff --git a/Userland/Libraries/LibWeb/DOM/Node.idl b/Userland/Libraries/LibWeb/DOM/Node.idl index b2f290f6c16..1ed330eba4e 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.idl +++ b/Userland/Libraries/LibWeb/DOM/Node.idl @@ -28,6 +28,7 @@ interface Node : EventTarget { // However, we only apply it to setters, so this works as a stop gap. // Replace this with something like a special cased [LegacyNullToEmptyString]. [LegacyNullToEmptyString, CEReactions] attribute DOMString? textContent; + [CEReactions] undefined normalize(); [CEReactions] Node appendChild(Node node); [ImplementedAs=pre_insert, CEReactions] Node insertBefore(Node node, Node? child);