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(""sv);
+ builder.append(tag_name);
+ 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(""sv);
- builder.append(tag_name);
- 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,