LibWeb: Update HTML fragment serialization for declarative shadow DOM

(cherry picked from commit e62db9c1181f5af49242656d3e065ceb0db800b9)
This commit is contained in:
Andreas Kling 2024-06-25 10:49:54 +02:00 committed by Nico Weber
parent a5189537e9
commit 88d9dc148c
3 changed files with 89 additions and 32 deletions

View file

@ -1352,17 +1352,17 @@ void Node::string_replace_all(String const& string)
replace_all(node); replace_all(node);
} }
// https://w3c.github.io/DOM-Parsing/#dfn-fragment-serializing-algorithm // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#fragment-serializing-algorithm-steps
WebIDL::ExceptionOr<String> Node::serialize_fragment(DOMParsing::RequireWellFormed require_well_formed, FragmentSerializationMode fragment_serialization_mode) 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. // 1. Let context document be the value of node's node document.
auto const& context_document = document(); auto const& context_document = document();
// 2. If context document is an HTML document, return an HTML serialization of node. // 2. If context document is an HTML document, return the result of HTML fragment serialization algorithm with node, false, and « ».
if (context_document.is_html_document()) if (context_document.is_html_document())
return HTML::HTMLParser::serialize_html_fragment(*this, fragment_serialization_mode); return HTML::HTMLParser::serialize_html_fragment(*this, HTML::HTMLParser::SerializableShadowRoots::No, {}, fragment_serialization_mode);
// 3. Otherwise, context document is an XML document; return an XML serialization of node passing the flag require well-formed. // 3. Return the XML serialization of node given require well-formed.
return DOMParsing::serialize_node_to_xml_string(*this, require_well_formed); return DOMParsing::serialize_node_to_xml_string(*this, require_well_formed);
} }

View file

@ -4411,7 +4411,7 @@ static String escape_string(StringView string, AttributeMode attribute_mode)
} }
// https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-serialisation-algorithm // https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-serialisation-algorithm
String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentSerializationMode fragment_serialization_mode) String HTMLParser::serialize_html_fragment(DOM::Node const& node, SerializableShadowRoots serializable_shadow_roots, Vector<JS::Handle<DOM::ShadowRoot>> const& shadow_roots, DOM::FragmentSerializationMode fragment_serialization_mode)
{ {
// NOTE: Steps in this function are jumbled a bit to accommodate the Element.outerHTML API. // 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, // When called with FragmentSerializationMode::Outer, we will serialize the element itself,
@ -4421,8 +4421,8 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
StringBuilder builder; StringBuilder builder;
auto serialize_element = [&](DOM::Element const& element) { 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. // 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. // Otherwise, let tagname be current node's qualified name.
FlyString tag_name; FlyString tag_name;
if (element.namespace_uri().has_value() && element.namespace_uri()->is_one_of(Namespace::HTML, Namespace::MathML, Namespace::SVG)) if (element.namespace_uri().has_value() && element.namespace_uri()->is_one_of(Namespace::HTML, Namespace::MathML, Namespace::SVG))
@ -4430,22 +4430,27 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
else else
tag_name = element.qualified_name(); tag_name = element.qualified_name();
// 2. Append a U+003C LESS-THAN SIGN character (<), followed by tagname. // Append a U+003C LESS-THAN SIGN character (<), followed by tagname.
builder.append('<'); builder.append('<');
builder.append(tag_name); 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, // 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, // then append the string " is="",
// followed by a U+0022 QUOTATION MARK character ("). // 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)) { if (element.is_value().has_value() && !element.has_attribute(AttributeNames::is)) {
builder.append(" is=\""sv); builder.append(" is=\""sv);
builder.append(escape_string(element.is_value().value(), AttributeMode::Yes)); builder.append(escape_string(element.is_value().value(), AttributeMode::Yes));
builder.append('"'); 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 (=), // For each attribute that the element has,
// 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 ("). // append a U+0020 SPACE character,
// NOTE: The order of attributes is implementation-defined. The only constraint is that the order must be stable. // 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 (").
element.for_each_attribute([&](auto const& attribute) { element.for_each_attribute([&](auto const& attribute) {
builder.append(' '); builder.append(' ');
@ -4472,16 +4477,20 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
builder.append('"'); builder.append('"');
}); });
// 5. Append a U+003E GREATER-THAN SIGN character (>). // Append a U+003E GREATER-THAN SIGN character (>).
builder.append('>'); builder.append('>');
// 6. If current node serializes as void, then continue on to the next child node at this point. // If current node serializes as void, then continue on to the next child node at this point.
if (element.serializes_as_void()) if (element.serializes_as_void())
return IterationDecision::Continue; 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), // Append the value of running the HTML fragment serialization algorithm with current node,
// 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 (>). // serializableShadowRoots, and shadowRoots (thus recursing into this algorithm for that node),
builder.append(serialize_html_fragment(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, serializable_shadow_roots, shadow_roots));
builder.append("</"sv); builder.append("</"sv);
builder.append(tag_name); builder.append(tag_name);
builder.append('>'); builder.append('>');
@ -4510,9 +4519,53 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
// (NOTE: This is out of order of the spec to avoid another dynamic cast. The second step just creates a string builder, so it shouldn't matter) // (NOTE: This is out of order of the spec to avoid another dynamic cast. The second step just creates a string builder, so it shouldn't matter)
if (is<HTML::HTMLTemplateElement>(element)) if (is<HTML::HTMLTemplateElement>(element))
actual_node = verify_cast<HTML::HTMLTemplateElement>(element).content(); actual_node = verify_cast<HTML::HTMLTemplateElement>(element).content();
// 4. If current node is a shadow host, then:
if (element.is_shadow_host()) {
// 1. Let shadow be current node's shadow root.
auto shadow = element.shadow_root();
// 2. If one of the following is true:
// - serializableShadowRoots is true and shadow's serializable is true; or
// - shadowRoots contains shadow,
if ((serializable_shadow_roots == SerializableShadowRoots::Yes && shadow->serializable())
|| shadow_roots.find_first_index_if([&](auto& entry) { return entry == shadow; }).has_value()) {
// then:
// 1. Append "<template shadowrootmode="".
builder.append("<template shadowrootmode=\""sv);
// 2. If shadow's mode is "open", then append "open". Otherwise, append "closed".
builder.append(shadow->mode() == Bindings::ShadowRootMode::Open ? "open"sv : "closed"sv);
// 3. Append """.
builder.append('"');
// 4. If shadow's delegates focus is set, then append " shadowrootdelegatesfocus=""".
if (shadow->delegates_focus())
builder.append(" shadowrootdelegatesfocus=\"\""sv);
// 5. If shadow's serializable is set, then append " shadowrootserializable=""".
if (shadow->serializable())
builder.append(" shadowrootserializable=\"\""sv);
// 6. If shadow's clonable is set, then append " shadowrootclonable=""".
if (shadow->clonable())
builder.append(" shadowrootclonable=\"\""sv);
// 7. Append ">".
builder.append('>');
// 8. Append the value of running the HTML fragment serialization algorithm with shadow,
// serializableShadowRoots, and shadowRoots (thus recursing into this algorithm for that element).
builder.append(serialize_html_fragment(*shadow, serializable_shadow_roots, shadow_roots));
// 9. Append "</template>".
builder.append("</template>"sv);
}
}
} }
// 4. For each child node of the node, in tree order, run the following steps: // 5. For each child node of the node, in tree order, run the following steps:
actual_node->for_each_child([&](DOM::Node& current_node) { actual_node->for_each_child([&](DOM::Node& current_node) {
// 1. Let current node be the child node being processed. // 1. Let current node be the child node being processed.
@ -4533,8 +4586,8 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
if (is<DOM::Element>(parent)) { if (is<DOM::Element>(parent)) {
auto& parent_element = verify_cast<DOM::Element>(*parent); auto& parent_element = verify_cast<DOM::Element>(*parent);
// 1. If the parent of current node is a style, script, xmp, iframe, noembed, noframes, or plaintext element, // If the parent of current node is a style, script, xmp, iframe, noembed, noframes, or plaintext element,
// or if the parent of current node is a noscript element and scripting is enabled for the node, then append the value of current node's data IDL attribute literally. // or if the parent of current node is a noscript element and scripting is enabled for the node, then append the value of current node's data IDL attribute literally.
if (parent_element.local_name().is_one_of(HTML::TagNames::style, HTML::TagNames::script, HTML::TagNames::xmp, HTML::TagNames::iframe, HTML::TagNames::noembed, HTML::TagNames::noframes, HTML::TagNames::plaintext) if (parent_element.local_name().is_one_of(HTML::TagNames::style, HTML::TagNames::script, HTML::TagNames::xmp, HTML::TagNames::iframe, HTML::TagNames::noembed, HTML::TagNames::noframes, HTML::TagNames::plaintext)
|| (parent_element.local_name() == HTML::TagNames::noscript && !parent_element.is_scripting_disabled())) { || (parent_element.local_name() == HTML::TagNames::noscript && !parent_element.is_scripting_disabled())) {
builder.append(text_node.data()); builder.append(text_node.data());
@ -4542,7 +4595,7 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
} }
} }
// 2. Otherwise, append the value of current node's data IDL attribute, escaped as described below. // Otherwise, append the value of current node's data IDL attribute, escaped as described below.
builder.append(escape_string(text_node.data(), AttributeMode::No)); builder.append(escape_string(text_node.data(), AttributeMode::No));
} }
@ -4550,8 +4603,8 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
// -> If current node is a Comment // -> If current node is a Comment
auto& comment_node = verify_cast<DOM::Comment>(current_node); auto& comment_node = verify_cast<DOM::Comment>(current_node);
// 1. Append the literal string "<!--" (U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS), // Append the literal string "<!--" (U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS),
// followed by the value of current node's data IDL attribute, followed by the literal string "-->" (U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN). // followed by the value of current node's data IDL attribute, followed by the literal string "-->" (U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN).
builder.append("<!--"sv); builder.append("<!--"sv);
builder.append(comment_node.data()); builder.append(comment_node.data());
builder.append("-->"sv); builder.append("-->"sv);
@ -4562,8 +4615,8 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
// -> If current node is a ProcessingInstruction // -> If current node is a ProcessingInstruction
auto& processing_instruction_node = verify_cast<DOM::ProcessingInstruction>(current_node); auto& processing_instruction_node = verify_cast<DOM::ProcessingInstruction>(current_node);
// 1. Append the literal string "<?" (U+003C LESS-THAN SIGN, U+003F QUESTION MARK), followed by the value of current node's target IDL attribute, // Append the literal string "<?" (U+003C LESS-THAN SIGN, U+003F QUESTION MARK), followed by the value of current node's target IDL attribute,
// followed by a single U+0020 SPACE character, followed by the value of current node's data IDL attribute, followed by a single U+003E GREATER-THAN SIGN character (>). // followed by a single U+0020 SPACE character, followed by the value of current node's data IDL attribute, followed by a single U+003E GREATER-THAN SIGN character (>).
builder.append("<?"sv); builder.append("<?"sv);
builder.append(processing_instruction_node.target()); builder.append(processing_instruction_node.target());
builder.append(' '); builder.append(' ');
@ -4576,9 +4629,9 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
// -> If current node is a DocumentType // -> If current node is a DocumentType
auto& document_type_node = verify_cast<DOM::DocumentType>(current_node); auto& document_type_node = verify_cast<DOM::DocumentType>(current_node);
// 1. Append the literal string "<!DOCTYPE" (U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+0044 LATIN CAPITAL LETTER D, U+004F LATIN CAPITAL LETTER O, // Append the literal string "<!DOCTYPE" (U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+0044 LATIN CAPITAL LETTER D, U+004F LATIN CAPITAL LETTER O,
// U+0043 LATIN CAPITAL LETTER C, U+0054 LATIN CAPITAL LETTER T, U+0059 LATIN CAPITAL LETTER Y, U+0050 LATIN CAPITAL LETTER P, U+0045 LATIN CAPITAL LETTER E), // U+0043 LATIN CAPITAL LETTER C, U+0054 LATIN CAPITAL LETTER T, U+0059 LATIN CAPITAL LETTER Y, U+0050 LATIN CAPITAL LETTER P, U+0045 LATIN CAPITAL LETTER E),
// followed by a space (U+0020 SPACE), followed by the value of current node's name IDL attribute, followed by the literal string ">" (U+003E GREATER-THAN SIGN). // followed by a space (U+0020 SPACE), followed by the value of current node's name IDL attribute, followed by the literal string ">" (U+003E GREATER-THAN SIGN).
builder.append("<!DOCTYPE "sv); builder.append("<!DOCTYPE "sv);
builder.append(document_type_node.name()); builder.append(document_type_node.name());
builder.append('>'); builder.append('>');
@ -4588,7 +4641,7 @@ String HTMLParser::serialize_html_fragment(DOM::Node const& node, DOM::FragmentS
return IterationDecision::Continue; return IterationDecision::Continue;
}); });
// 5. Return s. // 6. Return s.
return MUST(builder.to_string()); return MUST(builder.to_string());
} }

View file

@ -61,7 +61,11 @@ public:
DOM::Document& document(); DOM::Document& document();
static Vector<JS::Handle<DOM::Node>> parse_html_fragment(DOM::Element& context_element, StringView); static Vector<JS::Handle<DOM::Node>> parse_html_fragment(DOM::Element& context_element, StringView);
static String serialize_html_fragment(DOM::Node const& node, DOM::FragmentSerializationMode = DOM::FragmentSerializationMode::Inner); enum class SerializableShadowRoots {
No,
Yes,
};
static String serialize_html_fragment(DOM::Node const&, SerializableShadowRoots, Vector<JS::Handle<DOM::ShadowRoot>> const&, DOM::FragmentSerializationMode = DOM::FragmentSerializationMode::Inner);
enum class InsertionMode { enum class InsertionMode {
#define __ENUMERATE_INSERTION_MODE(mode) mode, #define __ENUMERATE_INSERTION_MODE(mode) mode,