Skip to content

Commit

Permalink
LibWeb: Implement Element.outerHTML
Browse files Browse the repository at this point in the history
This piggybacks on the same fragment serialization code that innerHTML
uses, but instead of constructing an imaginary parent element like the
spec asks us to, we just add a separate serialization mode that includes
the context element in the serialized markup.

This makes the image carousel on https://utah.edu/ show up :^)
  • Loading branch information
awesomekling committed Apr 9, 2024
1 parent d887387 commit 3a687e7
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 75 deletions.
2 changes: 2 additions & 0 deletions Tests/LibWeb/Text/expected/HTML/outerHTML.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello students outerHTML: <div id="foo"><b>hello students</b></div>
innerHTML: <b>hello students</b>
8 changes: 8 additions & 0 deletions Tests/LibWeb/Text/input/HTML/outerHTML.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script src="../include.js"></script>
<div id="foo"><b>hello students</b></div>
<script>
test(() => {
println("outerHTML: " + foo.outerHTML)
println("innerHTML: " + foo.innerHTML)
});
</script>
13 changes: 13 additions & 0 deletions Userland/Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,19 @@ bool Element::is_actually_disabled() const
return false;
}

// https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml
WebIDL::ExceptionOr<String> Element::outer_html() const
{
return serialize_fragment(DOMParsing::RequireWellFormed::Yes, FragmentSerializationMode::Outer);
}

// https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml
WebIDL::ExceptionOr<void> 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<void> Element::insert_adjacent_html(String const& position, String const& text)
{
Expand Down
3 changes: 3 additions & 0 deletions Userland/Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ class Element

WebIDL::ExceptionOr<void> insert_adjacent_html(String const& position, String const& text);

WebIDL::ExceptionOr<String> outer_html() const;
WebIDL::ExceptionOr<void> set_outer_html(String const&);

bool is_focused() const;
bool is_active() const;
bool is_target() const;
Expand Down
2 changes: 2 additions & 0 deletions Userland/Libraries/LibWeb/DOM/Element.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {});
Expand Down
4 changes: 2 additions & 2 deletions Userland/Libraries/LibWeb/DOM/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1336,14 +1336,14 @@ void Node::string_replace_all(String const& string)
}

// https://w3c.github.io/DOM-Parsing/#dfn-fragment-serializing-algorithm
WebIDL::ExceptionOr<String> Node::serialize_fragment(DOMParsing::RequireWellFormed require_well_formed) const
WebIDL::ExceptionOr<String> 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);
Expand Down
7 changes: 6 additions & 1 deletion Userland/Libraries/LibWeb/DOM/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ struct GetRootNodeOptions {
bool composed { false };
};

enum class FragmentSerializationMode {
Inner,
Outer,
};

class Node : public EventTarget {
WEB_PLATFORM_OBJECT(Node, EventTarget);

Expand Down Expand Up @@ -242,7 +247,7 @@ class Node : public EventTarget {
i32 unique_id() const { return m_unique_id; }
static Node* from_unique_id(i32);

WebIDL::ExceptionOr<String> serialize_fragment(DOMParsing::RequireWellFormed) const;
WebIDL::ExceptionOr<String> serialize_fragment(DOMParsing::RequireWellFormed, FragmentSerializationMode = FragmentSerializationMode::Inner) const;

void replace_all(JS::GCPtr<Node>);
void string_replace_all(String const&);
Expand Down
154 changes: 83 additions & 71 deletions Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<DOM::Element>(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<DOM::Node const> actual_node = node;
Expand All @@ -4295,9 +4376,6 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node)
actual_node = verify_cast<HTML::HTMLTemplateElement>(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.
Expand All @@ -4307,73 +4385,7 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node)
if (is<DOM::Element>(current_node)) {
// -> If current node is an Element
auto& element = verify_cast<DOM::Element>(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<DOM::Text>(current_node)) {
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class HTMLParser final : public JS::Cell {
DOM::Document& document();

static Vector<JS::Handle<DOM::Node>> 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,
Expand Down

0 comments on commit 3a687e7

Please sign in to comment.