Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax <select> parser (WHATWG proposal) #560

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions html5ever/src/tree_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@ where
n
}

/// Pop element until an element with the given name has been popped.
fn pop_until_named(&self, name: LocalName) -> usize {
self.pop_until(|p| *p.ns == ns!(html) && *p.local == name)
}
Expand Down Expand Up @@ -1231,16 +1232,6 @@ where
_ => continue,
};
match *name {
local_name!("select") => {
for ancestor in self.open_elems.borrow()[0..i].iter().rev() {
if self.html_elem_named(ancestor, local_name!("template")) {
return InSelect;
} else if self.html_elem_named(ancestor, local_name!("table")) {
return InSelectInTable;
}
}
return InSelect;
},
local_name!("td") | local_name!("th") => {
if !last {
return InCell;
Expand Down
168 changes: 42 additions & 126 deletions html5ever/src/tree_builder/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,13 @@ where

tag @ <hr> => {
self.close_p_element_in_button_scope();
if self.in_scope_named(default_scope, local_name!("select")) {
self.generate_implied_end_except(local_name!("optgroup"));
if self.in_scope_named(default_scope, local_name!("option")) {
self.sink.parse_error(Borrowed("hr in option"));
}
}

self.insert_and_pop_element_for(tag);
self.frameset_ok.set(false);
DoneAckSelfClosing
Expand Down Expand Up @@ -662,24 +669,48 @@ where
// <noscript> handled in wildcard case below

tag @ <select> => {
if self.in_scope_named(default_scope, local_name!("select")) {
self.sink.parse_error(Borrowed("nested select"));
self.pop_until_named(local_name!("select"));
}

self.reconstruct_formatting();
self.insert_element_for(tag);
self.frameset_ok.set(false);
// NB: mode == InBody but possibly self.mode != mode, if
// we're processing "as in the rules for InBody".
self.mode.set(match self.mode.get() {
InTable | InCaption | InTableBody
| InRow | InCell => InSelectInTable,
_ => InSelect,
});
Done
}

tag @ <optgroup> <option> => {
if self.current_node_named(local_name!("option")) {
self.pop();
tag @ <option> => {
if self.in_scope_named(default_scope, local_name!("select")) {
self.generate_implied_end_except(local_name!("optgroup"));
if self.in_scope_named(default_scope, local_name!("option")) {
self.sink.parse_error(Borrowed("nested options"));
}
} else {
if self.current_node_named(local_name!("option")) {
self.pop();
}
self.reconstruct_formatting();
}
self.reconstruct_formatting();

self.insert_element_for(tag);
Done
}

tag @ <optgroup> => {
if self.in_scope_named(default_scope, local_name!("select")) {
self.generate_implied_end(cursory_implied_end);
// XXX: perf
if self.in_scope_named(default_scope, local_name!("option")) || self.in_scope_named(default_scope, local_name!("optgroup")) {
self.sink.parse_error(Borrowed("nested options"));
}
} else {
if self.current_node_named(local_name!("option")) {
self.pop();
}
self.reconstruct_formatting();
}

self.insert_element_for(tag);
Done
}
Expand Down Expand Up @@ -1100,121 +1131,6 @@ where
token => self.step(InBody, token),
}),

//§ parsing-main-inselect
InSelect => match_token!(token {
NullCharacterToken => self.unexpected(&token),
CharacterTokens(_, text) => self.append_text(text),
CommentToken(text) => self.append_comment(text),

<html> => self.step(InBody, token),

tag @ <option> => {
if self.current_node_named(local_name!("option")) {
self.pop();
}
self.insert_element_for(tag);
Done
}

tag @ <optgroup> => {
if self.current_node_named(local_name!("option")) {
self.pop();
}
if self.current_node_named(local_name!("optgroup")) {
self.pop();
}
self.insert_element_for(tag);
Done
}

tag @ <hr> => {
if self.current_node_named(local_name!("option")) {
self.pop();
}
if self.current_node_named(local_name!("optgroup")) {
self.pop();
}
self.insert_element_for(tag);
self.pop();
DoneAckSelfClosing
}

</optgroup> => {
if self.open_elems.borrow().len() >= 2
&& self.current_node_named(local_name!("option"))
&& self.html_elem_named(&self.open_elems.borrow()[self.open_elems.borrow().len() - 2],
local_name!("optgroup")) {
self.pop();
}
if self.current_node_named(local_name!("optgroup")) {
self.pop();
} else {
self.unexpected(&token);
}
Done
}

</option> => {
if self.current_node_named(local_name!("option")) {
self.pop();
} else {
self.unexpected(&token);
}
Done
}

tag @ <select> </select> => {
let in_scope = self.in_scope_named(select_scope, local_name!("select"));

if !in_scope || tag.kind == StartTag {
self.unexpected(&tag);
}

if in_scope {
self.pop_until_named(local_name!("select"));
self.mode.set(self.reset_insertion_mode());
}
Done
}

<input> <keygen> <textarea> => {
self.unexpected(&token);
if self.in_scope_named(select_scope, local_name!("select")) {
self.pop_until_named(local_name!("select"));
Reprocess(self.reset_insertion_mode(), token)
} else {
Done
}
}

<script> <template> </template> => self.step(InHead, token),

EOFToken => self.step(InBody, token),

token => self.unexpected(&token),
}),

//§ parsing-main-inselectintable
InSelectInTable => match_token!(token {
<caption> <table> <tbody> <tfoot> <thead> <tr> <td> <th> => {
self.unexpected(&token);
self.pop_until_named(local_name!("select"));
Reprocess(self.reset_insertion_mode(), token)
}

tag @ </caption> </table> </tbody> </tfoot> </thead> </tr> </td> </th> => {
self.unexpected(&tag);
if self.in_scope_named(table_scope, tag.name.clone()) {
self.pop_until_named(local_name!("select"));
Reprocess(self.reset_insertion_mode(), TagToken(tag))
} else {
Done
}
}

token => self.step(InSelect, token),
}),

//§ parsing-main-intemplate
InTemplate => match_token!(token {
CharacterTokens(_, _) => self.step(InBody, token),
Expand Down
7 changes: 1 addition & 6 deletions html5ever/src/tree_builder/tag_sets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,9 @@ macro_rules! declare_tag_set (
pub(crate) fn empty_set(_: ExpandedName) -> bool {
false
}
#[inline(always)]
pub(crate) fn full_set(_: ExpandedName) -> bool {
true
}

declare_tag_set!(pub html_default_scope =
"applet" "caption" "html" "table" "td" "th" "marquee" "object" "template");
"applet" "caption" "html" "table" "td" "th" "marquee" "object" "select" "template");

#[inline(always)]
pub(crate) fn default_scope(name: ExpandedName) -> bool {
Expand All @@ -66,7 +62,6 @@ pub(crate) fn default_scope(name: ExpandedName) -> bool {
declare_tag_set!(pub list_item_scope = [default_scope] + "ol" "ul");
declare_tag_set!(pub button_scope = [default_scope] + "button");
declare_tag_set!(pub table_scope = "html" "table" "template");
declare_tag_set!(pub select_scope = [full_set] - "optgroup" "option");

declare_tag_set!(pub table_body_context = "tbody" "tfoot" "thead" "template" "html");
declare_tag_set!(pub table_row_context = "tr" "template" "html");
Expand Down
2 changes: 0 additions & 2 deletions html5ever/src/tree_builder/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ pub(crate) enum InsertionMode {
InTableBody,
InRow,
InCell,
InSelect,
InSelectInTable,
InTemplate,
AfterBody,
InFrameset,
Expand Down
12 changes: 7 additions & 5 deletions rcdom/tests/html-tree-builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,16 @@ fn make_test_desc_with_scripting_flag(
fields: &HashMap<String, String>,
scripting_enabled: bool,
) -> Test {
let get_field = |key| {
let field = fields.get(key).expect("missing field");
field.trim_end_matches('\n').to_string()
let expect_field = |key| {
fields
.get(key)
.unwrap_or_else(|| panic!("missing field {}, testcase: {:?}", key, fields))
.to_string()
};

let mut data = fields.get("data").expect("missing data").to_string();
let mut data = expect_field("data");
data.pop();
let expected = get_field("document");
let expected = expect_field("document").trim_end_matches('\n').to_string();
let context = fields
.get("document-fragment")
.map(|field| context_name(field.trim_end_matches('\n')));
Expand Down
Loading