LibWeb: Implement relaxed parser for <select>

- Allow general content within select element
- Update processing of options for select

See https://github.com/whatwg/html/pull/10557
This commit is contained in:
Luke Warlow 2024-12-04 21:09:08 +00:00
parent d4d335ebda
commit 7f9a10913f
No known key found for this signature in database
10 changed files with 247 additions and 309 deletions

View file

@ -36,14 +36,4 @@ void HTMLOptGroupElement::inserted()
static_cast<HTMLSelectElement&>(*parent()).update_selectedness();
}
void HTMLOptGroupElement::removed_from(Node* old_parent)
{
Base::removed_from(old_parent);
// The optgroup HTML element removing steps, given removedNode and oldParent, are:
// 1. If oldParent is a select element and removedNode has an option child, then run oldParent's selectedness setting algorithm.
if (old_parent && is<HTMLSelectElement>(*old_parent) && first_child_of_type<HTMLOptionElement>())
static_cast<HTMLSelectElement&>(*old_parent).update_selectedness();
}
}

View file

@ -25,7 +25,6 @@ private:
HTMLOptGroupElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
virtual void removed_from(Node*) override;
virtual void inserted() override;
};

View file

@ -205,28 +205,29 @@ void HTMLOptionElement::inserted()
set_selected_internal(selected());
// 1. The option HTML element insertion steps, given insertedNode, are:
// If insertedNode's parent is a select element,
// or insertedNode's parent is an optgroup element whose parent is a select element,
// then run that select element's selectedness setting algorithm.
if (is<HTMLSelectElement>(*parent()))
static_cast<HTMLSelectElement&>(*parent()).update_selectedness();
else if (is<HTMLOptGroupElement>(parent()) && parent()->parent() && is<HTMLSelectElement>(*parent()->parent()))
static_cast<HTMLSelectElement&>(*parent()->parent()).update_selectedness();
// The option HTML element insertion steps, given insertedOption, are:
// 1. For each ancestor of insertedOption's ancestors in reverse tree order:
for (auto* ancestor = parent_node(); ancestor; ancestor = ancestor->parent_node()) {
// 1. If ancestor is a select element, then run the selectedness setting algorithm given ancestor and return.
if (is<HTMLSelectElement>(*ancestor)) {
static_cast<HTMLSelectElement&>(*ancestor).update_selectedness();
return;
}
}
}
void HTMLOptionElement::removed_from(Node* old_parent)
{
Base::removed_from(old_parent);
// The option HTML element removing steps, given removedNode and oldParent, are:
// 1. If oldParent is a select element, or oldParent is an optgroup element whose parent is a select element,
// then run that select element's selectedness setting algorithm.
if (old_parent) {
if (is<HTMLSelectElement>(*old_parent))
static_cast<HTMLSelectElement&>(*old_parent).update_selectedness();
else if (is<HTMLOptGroupElement>(*old_parent) && old_parent->parent_element() && is<HTMLSelectElement>(old_parent->parent_element()))
static_cast<HTMLSelectElement&>(*old_parent->parent_element()).update_selectedness();
// The option HTML element removing steps, given removedOption and oldParent, are:
// 1. For each ancestor of oldParent's inclusive ancestors in reverse tree order:
for (auto* ancestor = old_parent; ancestor; ancestor = ancestor->parent_node()) {
// 1. If ancestor is a select element, then run the selectedness setting algorithm given ancestor and return.
if (is<HTMLSelectElement>(*ancestor)) {
static_cast<HTMLSelectElement&>(*ancestor).update_selectedness();
return;
}
}
}

View file

@ -199,24 +199,20 @@ GC::Ref<DOM::HTMLCollection> HTMLSelectElement::selected_options()
// https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-list
Vector<GC::Root<HTMLOptionElement>> HTMLSelectElement::list_of_options() const
{
// The list of options for a select element consists of all the option element children of the select element,
// and all the option element children of all the optgroup element children of the select element, in tree order.
Vector<GC::Root<HTMLOptionElement>> list;
// 1. Let options be « ».
Vector<GC::Root<HTMLOptionElement>> options;
// 2. For each node of select's descendants in tree order except the descendants which are select elements and their subtrees:
for_each_in_subtree([&](auto& node) {
if (is<HTMLSelectElement>(node))
return TraversalDecision::Break;
for_each_child_of_type<HTMLOptionElement>([&](HTMLOptionElement& option_element) {
list.append(GC::make_root(option_element));
return IterationDecision::Continue;
// 1. If node is an option element, then append node to options.
if (is<HTMLOptionElement>(node))
options.append(GC::make_root(const_cast<HTMLOptionElement&>(static_cast<HTMLOptionElement const&>(node))));
return TraversalDecision::Continue;
});
for_each_child_of_type<HTMLOptGroupElement>([&](HTMLOptGroupElement const& optgroup_element) {
optgroup_element.for_each_child_of_type<HTMLOptionElement>([&](HTMLOptionElement& option_element) {
list.append(GC::make_root(option_element));
return IterationDecision::Continue;
});
return IterationDecision::Continue;
});
return list;
// 3. Return options.
return options;
}
// https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element:concept-form-reset-control

View file

@ -434,12 +434,6 @@ void HTMLParser::process_using_the_rules_for(InsertionMode mode, HTMLToken& toke
case InsertionMode::InTableText:
handle_in_table_text(token);
break;
case InsertionMode::InSelectInTable:
handle_in_select_in_table(token);
break;
case InsertionMode::InSelect:
handle_in_select(token);
break;
case InsertionMode::InCaption:
handle_in_caption(token);
break;
@ -2164,7 +2158,7 @@ void HTMLParser::handle_in_body(HTMLToken& token)
}
// -> An end tag whose tag name is one of: "address", "article", "aside", "blockquote", "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "search", "section", "summary", "ul"
if (token.is_end_tag() && token.tag_name().is_one_of(HTML::TagNames::address, HTML::TagNames::article, HTML::TagNames::aside, HTML::TagNames::blockquote, HTML::TagNames::button, HTML::TagNames::center, HTML::TagNames::details, HTML::TagNames::dialog, HTML::TagNames::dir, HTML::TagNames::div, HTML::TagNames::dl, HTML::TagNames::fieldset, HTML::TagNames::figcaption, HTML::TagNames::figure, HTML::TagNames::footer, HTML::TagNames::header, HTML::TagNames::hgroup, HTML::TagNames::listing, HTML::TagNames::main, HTML::TagNames::menu, HTML::TagNames::nav, HTML::TagNames::ol, HTML::TagNames::pre, HTML::TagNames::search, HTML::TagNames::section, HTML::TagNames::summary, HTML::TagNames::ul)) {
if (token.is_end_tag() && token.tag_name().is_one_of(HTML::TagNames::address, HTML::TagNames::article, HTML::TagNames::aside, HTML::TagNames::blockquote, HTML::TagNames::button, HTML::TagNames::center, HTML::TagNames::details, HTML::TagNames::dialog, HTML::TagNames::dir, HTML::TagNames::div, HTML::TagNames::dl, HTML::TagNames::fieldset, HTML::TagNames::figcaption, HTML::TagNames::figure, HTML::TagNames::footer, HTML::TagNames::header, HTML::TagNames::hgroup, HTML::TagNames::listing, HTML::TagNames::main, HTML::TagNames::menu, HTML::TagNames::nav, HTML::TagNames::ol, HTML::TagNames::pre, HTML::TagNames::search, HTML::TagNames::section, HTML::TagNames::summary, HTML::TagNames::ul, HTML::TagNames::select)) {
// If the stack of open elements does not have an element in scope that is an HTML element with the same tag name as that of the token, then this is a parse error; ignore the token.
if (!m_stack_of_open_elements.has_in_scope(token.tag_name())) {
log_parse_error();
@ -2514,6 +2508,16 @@ void HTMLParser::handle_in_body(HTMLToken& token)
if (m_stack_of_open_elements.has_in_button_scope(HTML::TagNames::p))
close_a_p_element();
// If the stack of open elements has a select element in scope, then:
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::select)) {
// 1. Generate implied end tags.
generate_implied_end_tags();
// 2. If the stack of open elements has an option element in scope or has an optgroup element in scope, then this is a parse error.
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::option) || m_stack_of_open_elements.has_in_scope(HTML::TagNames::optgroup))
log_parse_error();
}
// Insert an HTML element for the token. Immediately pop the current node off the stack of open elements.
(void)insert_html_element(token);
(void)m_stack_of_open_elements.pop();
@ -2606,35 +2610,65 @@ void HTMLParser::handle_in_body(HTMLToken& token)
// -> A start tag whose tag name is "select"
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::select) {
// If the stack of open elements has a select element in scope then:
if (m_stack_of_open_elements.has_in_scope(token.tag_name())) {
// 1. Parse error.
log_parse_error();
// 2. Pop elements from the stack of open elements until a select element has been popped from the stack.
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(token.tag_name());
}
// Otherwise
else {
// Reconstruct the active formatting elements, if any.
reconstruct_the_active_formatting_elements();
// Insert an HTML element for the token.
(void)insert_html_element(token);
// Set the frameset-ok flag to "not ok".
m_frameset_ok = false;
}
return;
}
// -> A start tag whose tag name is "option"
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::option) {
// If the stack of open elements has a select element in scope, then:
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::select)) {
// 1. Generate implied end tags except for optgroup elements.
generate_implied_end_tags(HTML::TagNames::optgroup);
// 2. If the stack of open elements has an option element in scope, then this is a parse error.
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::option))
log_parse_error();
}
// Otherwise:
else {
// 1. If the current node is an option element, then pop the current node off the stack of open elements.
if (current_node()->local_name() == HTML::TagNames::option)
(void)m_stack_of_open_elements.pop();
}
// Reconstruct the active formatting elements, if any.
reconstruct_the_active_formatting_elements();
// Insert an HTML element for the token.
(void)insert_html_element(token);
// Set the frameset-ok flag to "not ok".
m_frameset_ok = false;
// If the insertion mode is one of "in table", "in caption", "in table body", "in row", or "in cell", then switch the insertion mode to "in select in table". Otherwise, switch the insertion mode to "in select".
switch (m_insertion_mode) {
case InsertionMode::InTable:
case InsertionMode::InCaption:
case InsertionMode::InTableBody:
case InsertionMode::InRow:
case InsertionMode::InCell:
m_insertion_mode = InsertionMode::InSelectInTable;
break;
default:
m_insertion_mode = InsertionMode::InSelect;
break;
}
return;
}
// -> A start tag whose tag name is one of: "optgroup", "option"
if (token.is_start_tag() && token.tag_name().is_one_of(HTML::TagNames::optgroup, HTML::TagNames::option)) {
// If the current node is an option element, then pop the current node off the stack of open elements.
if (current_node()->local_name() == HTML::TagNames::option)
// -> A start tag whose tag name is "optgroup"
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::optgroup) {
// If the stack of open elements has a select element in scope, then:
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::select)) {
// 1. Generate implied end tags.
generate_implied_end_tags();
// 2. If the stack of open elements has an option element in scope or has an optgroup element in scope, then this is a parse error.
if (m_stack_of_open_elements.has_in_scope(HTML::TagNames::option) || m_stack_of_open_elements.has_in_scope(HTML::TagNames::optgroup))
log_parse_error();
}
// Otherwise, if the current node is an option element, then pop the current node from the stack of open elements.
else if (current_node()->local_name() == HTML::TagNames::option)
(void)m_stack_of_open_elements.pop();
// Reconstruct the active formatting elements, if any.
@ -3582,173 +3616,6 @@ AnythingElse:
m_foster_parenting = false;
}
void HTMLParser::handle_in_select_in_table(HTMLToken& token)
{
if (token.is_start_tag() && token.tag_name().is_one_of(HTML::TagNames::caption, HTML::TagNames::table, HTML::TagNames::tbody, HTML::TagNames::tfoot, HTML::TagNames::thead, HTML::TagNames::tr, HTML::TagNames::td, HTML::TagNames::th)) {
log_parse_error();
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::select);
reset_the_insertion_mode_appropriately();
process_using_the_rules_for(m_insertion_mode, token);
return;
}
if (token.is_end_tag() && token.tag_name().is_one_of(HTML::TagNames::caption, HTML::TagNames::table, HTML::TagNames::tbody, HTML::TagNames::tfoot, HTML::TagNames::thead, HTML::TagNames::tr, HTML::TagNames::td, HTML::TagNames::th)) {
log_parse_error();
if (!m_stack_of_open_elements.has_in_table_scope(token.tag_name()))
return;
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::select);
reset_the_insertion_mode_appropriately();
process_using_the_rules_for(m_insertion_mode, token);
return;
}
process_using_the_rules_for(InsertionMode::InSelect, token);
}
void HTMLParser::handle_in_select(HTMLToken& token)
{
if (token.is_character()) {
if (token.code_point() == 0) {
log_parse_error();
return;
}
insert_character(token.code_point());
return;
}
if (token.is_comment()) {
insert_comment(token);
return;
}
if (token.is_doctype()) {
log_parse_error();
return;
}
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::html) {
process_using_the_rules_for(InsertionMode::InBody, token);
return;
}
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::option) {
if (current_node()->local_name() == HTML::TagNames::option) {
(void)m_stack_of_open_elements.pop();
}
(void)insert_html_element(token);
return;
}
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::optgroup) {
if (current_node()->local_name() == HTML::TagNames::option) {
(void)m_stack_of_open_elements.pop();
}
if (current_node()->local_name() == HTML::TagNames::optgroup) {
(void)m_stack_of_open_elements.pop();
}
(void)insert_html_element(token);
return;
}
// -> A start tag whose tag name is "hr"
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::hr) {
// If the current node is an option element, pop that node from the stack of open elements.
if (current_node()->local_name() == HTML::TagNames::option) {
(void)m_stack_of_open_elements.pop();
}
// If the current node is an optgroup element, pop that node from the stack of open elements.
if (current_node()->local_name() == HTML::TagNames::optgroup) {
(void)m_stack_of_open_elements.pop();
}
// Insert an HTML element for the token. Immediately pop the current node off the stack of open elements.
(void)insert_html_element(token);
(void)m_stack_of_open_elements.pop();
// Acknowledge the token's self-closing flag, if it is set.
token.acknowledge_self_closing_flag_if_set();
return;
}
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::optgroup) {
if (current_node()->local_name() == HTML::TagNames::option && node_before_current_node()->local_name() == HTML::TagNames::optgroup)
(void)m_stack_of_open_elements.pop();
if (current_node()->local_name() == HTML::TagNames::optgroup) {
(void)m_stack_of_open_elements.pop();
} else {
log_parse_error();
return;
}
return;
}
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::option) {
if (current_node()->local_name() == HTML::TagNames::option) {
(void)m_stack_of_open_elements.pop();
} else {
log_parse_error();
return;
}
return;
}
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::select) {
if (!m_stack_of_open_elements.has_in_select_scope(HTML::TagNames::select)) {
VERIFY(m_parsing_fragment);
log_parse_error();
return;
}
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::select);
reset_the_insertion_mode_appropriately();
return;
}
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::select) {
log_parse_error();
if (!m_stack_of_open_elements.has_in_select_scope(HTML::TagNames::select)) {
VERIFY(m_parsing_fragment);
return;
}
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::select);
reset_the_insertion_mode_appropriately();
return;
}
if (token.is_start_tag() && token.tag_name().is_one_of(HTML::TagNames::input, HTML::TagNames::keygen, HTML::TagNames::textarea)) {
log_parse_error();
if (!m_stack_of_open_elements.has_in_select_scope(HTML::TagNames::select)) {
VERIFY(m_parsing_fragment);
return;
}
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::select);
reset_the_insertion_mode_appropriately();
process_using_the_rules_for(m_insertion_mode, token);
return;
}
if (token.is_start_tag() && token.tag_name().is_one_of(HTML::TagNames::script, HTML::TagNames::template_)) {
process_using_the_rules_for(InsertionMode::InHead, token);
return;
}
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::template_) {
process_using_the_rules_for(InsertionMode::InHead, token);
return;
}
if (token.is_end_of_file()) {
process_using_the_rules_for(InsertionMode::InBody, token);
return;
}
log_parse_error();
}
void HTMLParser::handle_in_caption(HTMLToken& token)
{
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::caption) {
@ -4348,25 +4215,6 @@ void HTMLParser::reset_the_insertion_mode_appropriately()
if (node->namespace_uri() != Namespace::HTML)
continue;
if (node->local_name() == HTML::TagNames::select) {
if (!last) {
for (ssize_t j = i; j > 0; --j) {
auto& ancestor = m_stack_of_open_elements.elements().at(j - 1);
if (is<HTMLTemplateElement>(*ancestor))
break;
if (is<HTMLTableElement>(*ancestor)) {
m_insertion_mode = InsertionMode::InSelectInTable;
return;
}
}
}
m_insertion_mode = InsertionMode::InSelect;
return;
}
if (!last && node->local_name().is_one_of(HTML::TagNames::td, HTML::TagNames::th)) {
m_insertion_mode = InsertionMode::InCell;
return;

View file

@ -16,29 +16,27 @@
namespace Web::HTML {
#define ENUMERATE_INSERTION_MODES \
__ENUMERATE_INSERTION_MODE(Initial) \
__ENUMERATE_INSERTION_MODE(BeforeHTML) \
__ENUMERATE_INSERTION_MODE(BeforeHead) \
__ENUMERATE_INSERTION_MODE(InHead) \
__ENUMERATE_INSERTION_MODE(InHeadNoscript) \
__ENUMERATE_INSERTION_MODE(AfterHead) \
__ENUMERATE_INSERTION_MODE(InBody) \
__ENUMERATE_INSERTION_MODE(Text) \
__ENUMERATE_INSERTION_MODE(InTable) \
__ENUMERATE_INSERTION_MODE(InTableText) \
__ENUMERATE_INSERTION_MODE(InCaption) \
__ENUMERATE_INSERTION_MODE(InColumnGroup) \
__ENUMERATE_INSERTION_MODE(InTableBody) \
__ENUMERATE_INSERTION_MODE(InRow) \
__ENUMERATE_INSERTION_MODE(InCell) \
__ENUMERATE_INSERTION_MODE(InSelect) \
__ENUMERATE_INSERTION_MODE(InSelectInTable) \
__ENUMERATE_INSERTION_MODE(InTemplate) \
__ENUMERATE_INSERTION_MODE(AfterBody) \
__ENUMERATE_INSERTION_MODE(InFrameset) \
__ENUMERATE_INSERTION_MODE(AfterFrameset) \
__ENUMERATE_INSERTION_MODE(AfterAfterBody) \
#define ENUMERATE_INSERTION_MODES \
__ENUMERATE_INSERTION_MODE(Initial) \
__ENUMERATE_INSERTION_MODE(BeforeHTML) \
__ENUMERATE_INSERTION_MODE(BeforeHead) \
__ENUMERATE_INSERTION_MODE(InHead) \
__ENUMERATE_INSERTION_MODE(InHeadNoscript) \
__ENUMERATE_INSERTION_MODE(AfterHead) \
__ENUMERATE_INSERTION_MODE(InBody) \
__ENUMERATE_INSERTION_MODE(Text) \
__ENUMERATE_INSERTION_MODE(InTable) \
__ENUMERATE_INSERTION_MODE(InTableText) \
__ENUMERATE_INSERTION_MODE(InCaption) \
__ENUMERATE_INSERTION_MODE(InColumnGroup) \
__ENUMERATE_INSERTION_MODE(InTableBody) \
__ENUMERATE_INSERTION_MODE(InRow) \
__ENUMERATE_INSERTION_MODE(InCell) \
__ENUMERATE_INSERTION_MODE(InTemplate) \
__ENUMERATE_INSERTION_MODE(AfterBody) \
__ENUMERATE_INSERTION_MODE(InFrameset) \
__ENUMERATE_INSERTION_MODE(AfterFrameset) \
__ENUMERATE_INSERTION_MODE(AfterAfterBody) \
__ENUMERATE_INSERTION_MODE(AfterAfterFrameset)
class HTMLParser final : public JS::Cell {
@ -116,8 +114,6 @@ private:
void handle_in_row(HTMLToken&);
void handle_in_cell(HTMLToken&);
void handle_in_table_text(HTMLToken&);
void handle_in_select_in_table(HTMLToken&);
void handle_in_select(HTMLToken&);
void handle_in_caption(HTMLToken&);
void handle_in_column_group(HTMLToken&);
void handle_in_template(HTMLToken&);

View file

@ -11,7 +11,7 @@
namespace Web::HTML {
static Vector<FlyString> s_base_list { "applet"_fly_string, "caption"_fly_string, "html"_fly_string, "table"_fly_string, "td"_fly_string, "th"_fly_string, "marquee"_fly_string, "object"_fly_string, "template"_fly_string };
static Vector<FlyString> s_base_list { "applet"_fly_string, "caption"_fly_string, "html"_fly_string, "table"_fly_string, "td"_fly_string, "th"_fly_string, "marquee"_fly_string, "object"_fly_string, "template"_fly_string, "select"_fly_string };
StackOfOpenElements::~StackOfOpenElements() = default;
@ -72,31 +72,6 @@ bool StackOfOpenElements::has_in_list_item_scope(FlyString const& tag_name) cons
return has_in_scope_impl(tag_name, list);
}
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-select-scope
// The stack of open elements is said to have a particular element in select scope
// when it has that element in the specific scope consisting of all element types except the following:
// - optgroup in the HTML namespace
// - option in the HTML namespace
// NOTE: In this case it's "all element types _except_"
bool StackOfOpenElements::has_in_select_scope(FlyString const& tag_name) const
{
// https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope
// 1. Initialize node to be the current node (the bottommost node of the stack).
for (auto& node : m_elements.in_reverse()) {
// 2. If node is the target node, terminate in a match state.
if (node->local_name() == tag_name)
return true;
// 3. Otherwise, if node is one of the element types in list, terminate in a failure state.
// NOTE: Here "list" refers to all elements except option and optgroup
if (node->local_name() != HTML::TagNames::option && node->local_name() != HTML::TagNames::optgroup)
return false;
// 4. Otherwise, set node to the previous entry in the stack of open elements and return to step 2.
}
// [4.] (This will never fail, since the loop will always terminate in the previous step if the top of the stack
// — an html element — is reached.)
VERIFY_NOT_REACHED();
}
bool StackOfOpenElements::contains(const DOM::Element& element) const
{
for (auto& element_on_stack : m_elements) {

View file

@ -39,7 +39,6 @@ public:
bool has_in_button_scope(FlyString const& tag_name) const;
bool has_in_table_scope(FlyString const& tag_name) const;
bool has_in_list_item_scope(FlyString const& tag_name) const;
bool has_in_select_scope(FlyString const& tag_name) const;
bool has_in_scope(const DOM::Element&) const;

View file

@ -0,0 +1,16 @@
Harness status: OK
Found 11 tests
11 Pass
Pass <div>s, <button>s, and <datalist>s should be allowed in <select>
Pass </select> should close <button>
Pass </select> should close <datalist>
Pass <select> in <button> in <select> should remove inner <select>
Pass <select> in <select><button><div> should remove inner <select>
Pass Divs and imgs should be allowed as direct children of select and within options without a datalist
Pass Input tags should parse inside select instead of closing the select
Pass textarea tags should parse inside select instead of closing the select
Pass The last test should not leave any tags open after parsing
Pass Nested selects should be retained 1
Pass Nested selects should be retained 2

View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/issues/9799">
<script src="../../../../../resources/testharness.js"></script>
<script src="../../../../../resources/testharnessreport.js"></script>
<body>
<select class=test
data-description='<div>s, <button>s, and <datalist>s should be allowed in <select>'
data-expect='
<div>div 1</div>
<button>button</button>
<div>div 2</div>
<datalist>
<option>option</option>
</datalist>
<div>div 3</div>
'>
<div>div 1</div>
<button>button</button>
<div>div 2</div>
<datalist>
<option>option</option>
</datalist>
<div>div 3</div>
</select>
<select class=test
data-description='</select> should close <button>'
data-expect='<button>button</button>'>
<button>button
</select>
<select class=test
data-description='</select> should close <datalist>'
data-expect='<datalist>datalist</datalist>'>
<datalist>datalist
</select>
<select id=nested1 class=test
data-description='<select> in <button> in <select> should remove inner <select>'
data-expect='<button></button>'>
<button>
<select id=expectafter1></select>
<div id=expectafter1b></div>
</button>
</select>
<select id=nested2 class=test
data-description='<select> in <select><button><div> should remove inner <select>'
data-expect='<button><div></div></button>'>
<button>
<div>
<select id=expectafter2>
</select>
<select class=test
data-description='Divs and imgs should be allowed as direct children of select and within options without a datalist'
data-expect='
<div>
<option><img>option</option>
</div>'>
<div>
<option><img>option</option>
</div>
</select>
<select class=test
data-description='Input tags should parse inside select instead of closing the select'
data-expect='<input>'>
<input>
</select>
<select class=test
data-description='textarea tags should parse inside select instead of closing the select'
data-expect='<textarea></textarea>'>
<textarea></textarea>
</select>
<div id=afterlast>
keep this div after the last test case
</div>
<script>
function removeWhitespace(t) {
return t.replace(/\s/g,'');
}
document.querySelectorAll('select.test').forEach(s => {
assert_true(!!s.dataset.description.length);
test(() => {
// The document.body check here and in the other tests is to make sure that a
// previous test case didn't leave the HTML parser open on another element.
assert_equals(s.parentNode, document.body);
assert_equals(removeWhitespace(s.innerHTML),removeWhitespace(s.dataset.expect));
},s.dataset.description)
});
test(() => {
assert_equals(document.getElementById('afterlast').parentNode, document.body);
}, 'The last test should not leave any tags open after parsing');
test(() => {
const outerSelect = document.getElementById('nested1');
const innerSelect = document.getElementById('expectafter1');
const nextDiv = document.getElementById('expectafter1b');
assert_true(!!outerSelect);
assert_equals(innerSelect, null,'Nested select should be removed');
assert_equals(outerSelect.nextElementSibling, nextDiv,'Subsequent content is there too');
}, 'Nested selects should be retained 1');
test(() => {
const outerSelect = document.getElementById('nested2');
const innerSelect = document.getElementById('expectafter2');
assert_true(!!outerSelect);
assert_equals(innerSelect, null,'Nested select should be pushed out as the next sibling');
}, 'Nested selects should be retained 2');
</script>