diff --git a/Tests/LibWeb/Text/expected/HTML/outerHTML.txt b/Tests/LibWeb/Text/expected/HTML/outerHTML.txt new file mode 100644 index 00000000000..4a0892c782c --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/outerHTML.txt @@ -0,0 +1,2 @@ +hello students outerHTML:
hello students
+innerHTML: hello students diff --git a/Tests/LibWeb/Text/input/HTML/outerHTML.html b/Tests/LibWeb/Text/input/HTML/outerHTML.html new file mode 100644 index 00000000000..8b56d5aca4b --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/outerHTML.html @@ -0,0 +1,8 @@ + +
hello students
+ diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp index e65095028f4..e52a980331b 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.cpp +++ b/Userland/Libraries/LibWeb/DOM/Element.cpp @@ -1402,6 +1402,19 @@ bool Element::is_actually_disabled() const return false; } +// https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml +WebIDL::ExceptionOr Element::outer_html() const +{ + return serialize_fragment(DOMParsing::RequireWellFormed::Yes, FragmentSerializationMode::Outer); +} + +// https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml +WebIDL::ExceptionOr Element::set_outer_html(String const&) +{ + dbgln("FIXME: Implement Element::set_outer_html()"); + return {}; +} + // https://w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml WebIDL::ExceptionOr Element::insert_adjacent_html(String const& position, String const& text) { diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h index 33385221feb..81526c0e957 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.h +++ b/Userland/Libraries/LibWeb/DOM/Element.h @@ -175,6 +175,9 @@ public: WebIDL::ExceptionOr insert_adjacent_html(String const& position, String const& text); + WebIDL::ExceptionOr outer_html() const; + WebIDL::ExceptionOr set_outer_html(String const&); + bool is_focused() const; bool is_active() const; bool is_target() const; diff --git a/Userland/Libraries/LibWeb/DOM/Element.idl b/Userland/Libraries/LibWeb/DOM/Element.idl index 390a04e9770..e13fd33a116 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.idl +++ b/Userland/Libraries/LibWeb/DOM/Element.idl @@ -85,6 +85,8 @@ interface Element : Node { undefined insertAdjacentText(DOMString where, DOMString data); [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text); + [CEReactions, LegacyNullToEmptyString] attribute DOMString outerHTML; + undefined scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg = {}); undefined scroll(optional ScrollToOptions options = {}); diff --git a/Userland/Libraries/LibWeb/DOM/Node.cpp b/Userland/Libraries/LibWeb/DOM/Node.cpp index 3e9e35d81d3..e0da6612b78 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.cpp +++ b/Userland/Libraries/LibWeb/DOM/Node.cpp @@ -1336,14 +1336,14 @@ void Node::string_replace_all(String const& string) } // https://w3c.github.io/DOM-Parsing/#dfn-fragment-serializing-algorithm -WebIDL::ExceptionOr Node::serialize_fragment(DOMParsing::RequireWellFormed require_well_formed) const +WebIDL::ExceptionOr Node::serialize_fragment(DOMParsing::RequireWellFormed require_well_formed, FragmentSerializationMode fragment_serialization_mode) const { // 1. Let context document be the value of node's node document. auto const& context_document = document(); // 2. If context document is an HTML document, return an HTML serialization of node. if (context_document.is_html_document()) - return HTML::HTMLParser::serialize_html_fragment(*this); + return HTML::HTMLParser::serialize_html_fragment(*this, fragment_serialization_mode); // 3. Otherwise, context document is an XML document; return an XML serialization of node passing the flag require well-formed. return DOMParsing::serialize_node_to_xml_string(*this, require_well_formed); diff --git a/Userland/Libraries/LibWeb/DOM/Node.h b/Userland/Libraries/LibWeb/DOM/Node.h index 4dc1d9d07bf..24284249fc2 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.h +++ b/Userland/Libraries/LibWeb/DOM/Node.h @@ -45,6 +45,11 @@ struct GetRootNodeOptions { bool composed { false }; }; +enum class FragmentSerializationMode { + Inner, + Outer, +}; + class Node : public EventTarget { WEB_PLATFORM_OBJECT(Node, EventTarget); @@ -242,7 +247,7 @@ public: i32 unique_id() const { return m_unique_id; } static Node* from_unique_id(i32); - WebIDL::ExceptionOr serialize_fragment(DOMParsing::RequireWellFormed) const; + WebIDL::ExceptionOr serialize_fragment(DOMParsing::RequireWellFormed, FragmentSerializationMode = FragmentSerializationMode::Inner) const; void replace_all(JS::GCPtr); void string_replace_all(String const&); diff --git a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index 0aa11a26ff4..af761cdcd92 100644 --- a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -4275,8 +4275,89 @@ static String escape_string(StringView string, AttributeMode attribute_mode) } // https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-serialisation-algorithm -String HTMLParser::serialize_html_fragment(DOM::Node const& node) +String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentSerializationMode fragment_serialization_mode) { + // NOTE: Steps in this function are jumbled a bit to accommodate the Element.outerHTML API. + // When called with FragmentSerializationMode::Outer, we will serialize the element itself, + // not just its children. + + // 2. Let s be a string, and initialize it to the empty string. + StringBuilder builder; + + auto serialize_element = [&](DOM::Element const& element) { + // 1. If current node is an element in the HTML namespace, the MathML namespace, or the SVG namespace, then let tagname be current node's local name. + // Otherwise, let tagname be current node's qualified name. + FlyString tag_name; + + if (element.namespace_uri().has_value() && element.namespace_uri()->is_one_of(Namespace::HTML, Namespace::MathML, Namespace::SVG)) + tag_name = element.local_name(); + else + tag_name = element.qualified_name(); + + // 2. Append a U+003C LESS-THAN SIGN character (<), followed by tagname. + builder.append('<'); + builder.append(tag_name); + + // 3. If current node's is value is not null, and the element does not have an is attribute in its attribute list, + // then append the string " is="", followed by current node's is value escaped as described below in attribute mode, + // followed by a U+0022 QUOTATION MARK character ("). + if (element.is_value().has_value() && !element.has_attribute(AttributeNames::is)) { + builder.append(" is=\""sv); + builder.append(escape_string(element.is_value().value(), AttributeMode::Yes)); + builder.append('"'); + } + + // 4. For each attribute that the element has, append a U+0020 SPACE character, the attribute's serialized name as described below, a U+003D EQUALS SIGN character (=), + // a U+0022 QUOTATION MARK character ("), the attribute's value, escaped as described below in attribute mode, and a second U+0022 QUOTATION MARK character ("). + // NOTE: The order of attributes is implementation-defined. The only constraint is that the order must be stable. + element.for_each_attribute([&](auto const& attribute) { + builder.append(' '); + + // An attribute's serialized name for the purposes of the previous paragraph must be determined as follows: + + // NOTE: As far as I can tell, these steps are equivalent to just using the qualified name. + // + // -> If the attribute has no namespace: + // The attribute's serialized name is the attribute's local name. + // -> If the attribute is in the XML namespace: + // The attribute's serialized name is the string "xml:" followed by the attribute's local name. + // -> If the attribute is in the XMLNS namespace and the attribute's local name is xmlns: + // The attribute's serialized name is the string "xmlns". + // -> If the attribute is in the XMLNS namespace and the attribute's local name is not xmlns: + // The attribute's serialized name is the string "xmlns:" followed by the attribute's local name. + // -> If the attribute is in the XLink namespace: + // The attribute's serialized name is the string "xlink:" followed by the attribute's local name. + // -> If the attribute is in some other namespace: + // The attribute's serialized name is the attribute's qualified name. + builder.append(attribute.name()); + + builder.append("=\""sv); + builder.append(escape_string(attribute.value(), AttributeMode::Yes)); + builder.append('"'); + }); + + // 5. Append a U+003E GREATER-THAN SIGN character (>). + builder.append('>'); + + // 6. If current node serializes as void, then continue on to the next child node at this point. + if (element.serializes_as_void()) + return IterationDecision::Continue; + + // 7. Append the value of running the HTML fragment serialization algorithm on the current node element (thus recursing into this algorithm for that element), + // followed by a U+003C LESS-THAN SIGN character (<), a U+002F SOLIDUS character (/), tagname again, and finally a U+003E GREATER-THAN SIGN character (>). + builder.append(serialize_html_fragment(element)); + builder.append("'); + + return IterationDecision::Continue; + }; + + if (fragment_serialization_mode == DOM::FragmentSerializationMode::Outer) { + serialize_element(verify_cast(node)); + return builder.to_string_without_validation(); + } + // The algorithm takes as input a DOM Element, Document, or DocumentFragment referred to as the node. VERIFY(node.is_element() || node.is_document() || node.is_document_fragment()); JS::NonnullGCPtr actual_node = node; @@ -4295,9 +4376,6 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node) actual_node = verify_cast(element).content(); } - // 2. Let s be a string, and initialize it to the empty string. - StringBuilder builder; - // 4. For each child node of the node, in tree order, run the following steps: actual_node->for_each_child([&](DOM::Node& current_node) { // 1. Let current node be the child node being processed. @@ -4307,73 +4385,7 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node) if (is(current_node)) { // -> If current node is an Element auto& element = verify_cast(current_node); - - // 1. If current node is an element in the HTML namespace, the MathML namespace, or the SVG namespace, then let tagname be current node's local name. - // Otherwise, let tagname be current node's qualified name. - FlyString tag_name; - - if (element.namespace_uri().has_value() && element.namespace_uri()->is_one_of(Namespace::HTML, Namespace::MathML, Namespace::SVG)) - tag_name = element.local_name(); - else - tag_name = element.qualified_name(); - - // 2. Append a U+003C LESS-THAN SIGN character (<), followed by tagname. - builder.append('<'); - builder.append(tag_name); - - // 3. If current node's is value is not null, and the element does not have an is attribute in its attribute list, - // then append the string " is="", followed by current node's is value escaped as described below in attribute mode, - // followed by a U+0022 QUOTATION MARK character ("). - if (element.is_value().has_value() && !element.has_attribute(AttributeNames::is)) { - builder.append(" is=\""sv); - builder.append(escape_string(element.is_value().value(), AttributeMode::Yes)); - builder.append('"'); - } - - // 4. For each attribute that the element has, append a U+0020 SPACE character, the attribute's serialized name as described below, a U+003D EQUALS SIGN character (=), - // a U+0022 QUOTATION MARK character ("), the attribute's value, escaped as described below in attribute mode, and a second U+0022 QUOTATION MARK character ("). - // NOTE: The order of attributes is implementation-defined. The only constraint is that the order must be stable. - element.for_each_attribute([&](auto const& attribute) { - builder.append(' '); - - // An attribute's serialized name for the purposes of the previous paragraph must be determined as follows: - - // NOTE: As far as I can tell, these steps are equivalent to just using the qualified name. - // - // -> If the attribute has no namespace: - // The attribute's serialized name is the attribute's local name. - // -> If the attribute is in the XML namespace: - // The attribute's serialized name is the string "xml:" followed by the attribute's local name. - // -> If the attribute is in the XMLNS namespace and the attribute's local name is xmlns: - // The attribute's serialized name is the string "xmlns". - // -> If the attribute is in the XMLNS namespace and the attribute's local name is not xmlns: - // The attribute's serialized name is the string "xmlns:" followed by the attribute's local name. - // -> If the attribute is in the XLink namespace: - // The attribute's serialized name is the string "xlink:" followed by the attribute's local name. - // -> If the attribute is in some other namespace: - // The attribute's serialized name is the attribute's qualified name. - builder.append(attribute.name()); - - builder.append("=\""sv); - builder.append(escape_string(attribute.value(), AttributeMode::Yes)); - builder.append('"'); - }); - - // 5. Append a U+003E GREATER-THAN SIGN character (>). - builder.append('>'); - - // 6. If current node serializes as void, then continue on to the next child node at this point. - if (element.serializes_as_void()) - return IterationDecision::Continue; - - // 7. Append the value of running the HTML fragment serialization algorithm on the current node element (thus recursing into this algorithm for that element), - // followed by a U+003C LESS-THAN SIGN character (<), a U+002F SOLIDUS character (/), tagname again, and finally a U+003E GREATER-THAN SIGN character (>). - builder.append(serialize_html_fragment(element)); - builder.append("'); - - return IterationDecision::Continue; + return serialize_element(element); } if (is(current_node)) { diff --git a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.h b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.h index c170a6d2442..0fab40d2fdc 100644 --- a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.h +++ b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.h @@ -61,7 +61,7 @@ public: DOM::Document& document(); static Vector> parse_html_fragment(DOM::Element& context_element, StringView); - static String serialize_html_fragment(DOM::Node const& node); + static String serialize_html_fragment(DOM::Node const& node, DOM::FragmentSerializationMode = DOM::FragmentSerializationMode::Inner); enum class InsertionMode { #define __ENUMERATE_INSERTION_MODE(mode) mode,