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
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
@@ -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)
}
@@ -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;
168 changes: 42 additions & 126 deletions html5ever/src/tree_builder/rules.rs
Original file line number Diff line number Diff line change
@@ -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
@@ -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
}
@@ -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),
7 changes: 1 addition & 6 deletions html5ever/src/tree_builder/tag_sets.rs
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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");
2 changes: 0 additions & 2 deletions html5ever/src/tree_builder/types.rs
Original file line number Diff line number Diff line change
@@ -38,8 +38,6 @@ pub(crate) enum InsertionMode {
InTableBody,
InRow,
InCell,
InSelect,
InSelectInTable,
InTemplate,
AfterBody,
InFrameset,
12 changes: 7 additions & 5 deletions rcdom/tests/html-tree-builder.rs
Original file line number Diff line number Diff line change
@@ -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')));
Loading