diff --git a/html5ever/Cargo.toml b/html5ever/Cargo.toml index 3caf8c61..cf4014d8 100644 --- a/html5ever/Cargo.toml +++ b/html5ever/Cargo.toml @@ -28,3 +28,7 @@ proc-macro2 = "1" [[bench]] name = "html5ever" harness = false + +[features] +# Always use this feature in new code. In the future it will become the default. +api_v2 = [] diff --git a/html5ever/examples/print-tree-actions.rs b/html5ever/examples/print-tree-actions.rs index 7ac2de17..74e1437f 100644 --- a/html5ever/examples/print-tree-actions.rs +++ b/html5ever/examples/print-tree-actions.rs @@ -22,6 +22,9 @@ use html5ever::tree_builder::{ }; use html5ever::{Attribute, ExpandedName, QualName}; +#[cfg(feature = "api_v2")] +use markup5ever::interface::tree_builder::SuperfluousClosingElement; + struct Sink { next_id: usize, names: HashMap, @@ -154,6 +157,13 @@ impl TreeSink for Sink { println!("Set current line to {}", line_number); } + #[cfg(feature = "api_v2")] + fn pop(&mut self, elem: &usize) -> Result<(), SuperfluousClosingElement> { + println!("Popped element {}", elem); + Ok(()) + } + + #[cfg(not(feature = "api_v2"))] fn pop(&mut self, elem: &usize) { println!("Popped element {}", elem); } diff --git a/html5ever/src/tree_builder/mod.rs b/html5ever/src/tree_builder/mod.rs index 5d392dbb..c4251263 100644 --- a/html5ever/src/tree_builder/mod.rs +++ b/html5ever/src/tree_builder/mod.rs @@ -11,6 +11,8 @@ //! The HTML5 tree builder. +use markup5ever::interface::tree_builder::SuperfluousClosingElement; + pub use crate::interface::{create_element, ElementFlags, NextParserState, Tracer, TreeSink}; pub use crate::interface::{AppendNode, AppendText, Attribute, NodeOrText}; pub use crate::interface::{LimitedQuirks, NoQuirks, Quirks, QuirksMode}; @@ -176,6 +178,15 @@ where } } + // TODO: Hack to prevent accessing empty stack. + fn pop_open_elem(&mut self) -> Option { + if self.open_elems.len() > 1 { + self.open_elems.pop() + } else { + None + } + } + /// Create a new tree builder which sends tree modifications to a particular `TreeSink`. /// This is for parsing fragments. /// @@ -398,29 +409,31 @@ where use self::tag_sets::*; declare_tag_set!(foster_target = "table" "tbody" "tfoot" "thead" "tr"); - let target = override_target.unwrap_or_else(|| self.current_node().clone()); - if !(self.foster_parenting && self.elem_in(&target, foster_target)) { - if self.html_elem_named(&target, local_name!("template")) { - // No foster parenting (inside template). - let contents = self.sink.get_template_contents(&target); - return LastChild(contents); - } else { - // No foster parenting (the common case). - return LastChild(target); + if let Some(current_node) = self.current_node_v2() { + let target = override_target.unwrap_or_else(|| current_node.clone()); + if !(self.foster_parenting && self.elem_in(&target, foster_target)) { + if self.html_elem_named(&target, local_name!("template")) { + // No foster parenting (inside template). + let contents = self.sink.get_template_contents(&target); + return LastChild(contents); + } else { + // No foster parenting (the common case). + return LastChild(target); + } } - } - // Foster parenting - let mut iter = self.open_elems.iter().rev().peekable(); - while let Some(elem) = iter.next() { - if self.html_elem_named(&elem, local_name!("template")) { - let contents = self.sink.get_template_contents(&elem); - return LastChild(contents); - } else if self.html_elem_named(&elem, local_name!("table")) { - return TableFosterParenting { - element: elem.clone(), - prev_element: (*iter.peek().unwrap()).clone(), - }; + // Foster parenting + let mut iter = self.open_elems.iter().rev().peekable(); + while let Some(elem) = iter.next() { + if self.html_elem_named(&elem, local_name!("template")) { + let contents = self.sink.get_template_contents(&elem); + return LastChild(contents); + } else if self.html_elem_named(&elem, local_name!("table")) { + return TableFosterParenting { + element: elem.clone(), + prev_element: (*iter.peek().unwrap()).clone(), + }; + } } } let html_elem = self.html_elem(); @@ -530,8 +543,15 @@ where } fn adjusted_current_node_present_but_not_in_html_namespace(&self) -> bool { - !self.open_elems.is_empty() && - self.sink.elem_name(self.adjusted_current_node()).ns != &ns!(html) + if !self.open_elems.is_empty() { + if let Some(current) = self.adjusted_current_node_v2() { + self.sink.elem_name(current).ns != &ns!(html) + } else { + false + } + } else { + false + } } } @@ -643,24 +663,71 @@ where } //ยง END + /// Indicate that a node was popped off the stack of open elements. + #[cfg(feature = "api_v2")] + fn current_node(&self) -> Option<&Handle> { + self.current_node_v2() + } + + /// Like current_node(), but in the case of no open element, just warn instead of returning an error. + #[deprecated(note = "You are using an outdated API. Please use api_v2 feature.")] + #[cfg(not(feature = "api_v2"))] fn current_node(&self) -> &Handle { self.open_elems.last().expect("no current element") } - fn adjusted_current_node(&self) -> &Handle { + #[deprecated(note = "You are using an outdated API. Please use api_v2 feature.")] + #[cfg(feature = "api_v2")] + /// Like current_node(), but in the case of no open element, just warn instead of returning an error. + fn current_node_unconditional(&self) -> &Handle { + if let Some(current) = self.current_node_v2() { + current + } else { + warn!("no current element"); + self.html_elem() + } + } + + #[cfg(not(feature = "api_v2"))] + /// Like current_node(), but in the case of no open element, just warn instead of returning an error. + fn current_node_unconditional(&self) -> &Handle { + self.current_node() + } + + #[doc(hidden)] + fn current_node_v2(&self) -> Option<&Handle> { + self.open_elems.last() + } + + #[cfg(feature = "api_v2")] + fn adjusted_current_node(&self) -> Option<&Handle> { + self.adjusted_current_node_v2() + } + + #[cfg(not(feature = "api_v2"))] + fn adjusted_current_node(&self) { + self.adjusted_current_node() + } + + #[doc(hidden)] + fn adjusted_current_node_v2(&self) -> Option<&Handle> { if self.open_elems.len() == 1 { if let Some(ctx) = self.context_elem.as_ref() { - return ctx; + return Some(ctx); } } - self.current_node() + self.current_node_v2() + } + + fn adjusted_current_node_unconditional(&self) -> &Handle { + self.adjusted_current_node_v2().expect("no current node") } fn current_node_in(&self, set: TagSet) -> bool where TagSet: Fn(ExpandedName) -> bool, { - set(self.sink.elem_name(self.current_node())) + set(self.sink.elem_name(self.current_node_unconditional())) // FIXME: Is using `current_node_unconditional()` correct? } // Insert at the "appropriate place for inserting a node". @@ -673,10 +740,10 @@ where // 1. if self.current_node_named(subject.clone()) { if self - .position_in_active_formatting(self.current_node()) + .position_in_active_formatting(self.current_node_unconditional()) // FIXME: Is using `current_node_unconditional()` correct? .is_none() { - self.pop(); + self.pop_unconditional(); // FIXME: Is using `current_node_unconditional()` correct? return; } } @@ -719,7 +786,7 @@ where } // 8. - if !self.sink.same_node(self.current_node(), &fmt_elem) { + if !self.sink.same_node(self.current_node_unconditional(), &fmt_elem) { // FIXME: Is using `current_node_unconditional()` correct? self.sink .parse_error(Borrowed("Formatting element not current node")); } @@ -879,10 +946,19 @@ where self.open_elems.push(elem.clone()); } - fn pop(&mut self) -> Handle { - let elem = self.open_elems.pop().expect("no current element"); - self.sink.pop(&elem); - elem + fn pop_v2(&mut self) -> Result { + if let Some(elem) = self.pop_open_elem() { + self.sink.pop(&elem); + Ok(elem) + } else { + Err(SuperfluousClosingElement::new()) + } + } + + fn pop_unconditional(&mut self) { + if self.pop_v2().is_err() { + warn!("no current element"); + } } fn remove_from_stack(&mut self, elem: &Handle) { @@ -1032,7 +1108,11 @@ where } fn current_node_named(&self, name: LocalName) -> bool { - self.html_elem_named(self.current_node(), name) + if let Some(current) = self.current_node_v2() { + self.html_elem_named(current, name) + } else { + false + } } fn in_scope_named(&self, scope: TagSet, name: LocalName) -> bool @@ -1055,7 +1135,7 @@ where return; } } - self.pop(); + self.pop_unconditional(); } } @@ -1079,7 +1159,7 @@ where if self.current_node_in(|x| pred(x)) { break; } - self.open_elems.pop(); + self.pop_open_elem(); } } @@ -1092,7 +1172,7 @@ where let mut n = 0; loop { n += 1; - match self.open_elems.pop() { + match self.pop_open_elem() { None => break, Some(elem) => { if pred(self.sink.elem_name(&elem)) { @@ -1431,7 +1511,11 @@ where return false; } - let name = self.sink.elem_name(self.adjusted_current_node()); + if self.adjusted_current_node_v2().is_none() { // hack + return false; + } + + let name = self.sink.elem_name(self.adjusted_current_node_unconditional()); if let ns!(html) = *name.ns { return false; } @@ -1466,9 +1550,13 @@ where .. }) => return false, CharacterTokens(..) | NullCharacterToken | TagToken(Tag { kind: StartTag, .. }) => { - return !self - .sink - .is_mathml_annotation_xml_integration_point(self.adjusted_current_node()); + if let Some(current) = self.adjusted_current_node_v2() { + return !self + .sink + .is_mathml_annotation_xml_integration_point(current); + } else { + return false; + } }, _ => {}, }; @@ -1640,7 +1728,7 @@ where } fn foreign_start_tag(&mut self, mut tag: Tag) -> ProcessResult { - let current_ns = self.sink.elem_name(self.adjusted_current_node()).ns.clone(); + let current_ns = self.sink.elem_name(self.adjusted_current_node_unconditional()).ns.clone(); match current_ns { ns!(mathml) => self.adjust_mathml_attributes(&mut tag), ns!(svg) => { @@ -1665,13 +1753,13 @@ where if self.is_fragment() { self.foreign_start_tag(tag) } else { - self.pop(); + self.pop_unconditional(); while !self.current_node_in(|n| { *n.ns == ns!(html) || mathml_text_integration_point(n) || svg_html_integration_point(n) }) { - self.pop(); + self.pop_unconditional(); } ReprocessForeign(TagToken(tag)) } diff --git a/html5ever/src/tree_builder/rules.rs b/html5ever/src/tree_builder/rules.rs index d9a4ba1f..10fc3cc5 100644 --- a/html5ever/src/tree_builder/rules.rs +++ b/html5ever/src/tree_builder/rules.rs @@ -138,7 +138,7 @@ where } => { - self.pop(); + self.pop_unconditional(); self.mode = AfterHead; Done } @@ -171,7 +171,7 @@ where tag @ => self.unexpected(&tag), token => { - self.pop(); + self.pop_unconditional(); Reprocess(AfterHead, token) } }), @@ -181,7 +181,7 @@ where => self.step(InBody, token), => { - self.pop(); + self.pop_unconditional(); self.mode = InHead; Done }, @@ -201,7 +201,7 @@ where token => { self.unexpected(&token); - self.pop(); + self.pop_unconditional(); Reprocess(InHead, token) }, }), @@ -353,7 +353,7 @@ where self.close_p_element_in_button_scope(); if self.current_node_in(heading_tag) { self.sink.parse_error(Borrowed("nested heading tags")); - self.pop(); + self.pop_unconditional(); } self.insert_element_for(tag); Done @@ -469,10 +469,11 @@ where return Done; } self.generate_implied_end(cursory_implied_end); - let current = self.current_node().clone(); - self.remove_from_stack(&node); - if !self.sink.same_node(¤t, &node) { - self.sink.parse_error(Borrowed("Bad open element on ")); + if let Some(current) = self.current_node_v2() { + self.remove_from_stack(&node); + if !self.sink.same_node(&self.current_node_unconditional().clone(), &node) { + self.sink.parse_error(Borrowed("Bad open element on ")); + } } } else { if !self.in_scope_named(default_scope, local_name!("form")) { @@ -666,7 +667,7 @@ where tag @ => { if self.current_node_named(local_name!("option")) { - self.pop(); + self.pop_unconditional(); } if self.current_node_named(local_name!("optgroup")) { - self.pop(); + self.pop_unconditional(); } self.insert_element_for(tag); Done @@ -1120,10 +1129,10 @@ where && self.current_node_named(local_name!("option")) && self.html_elem_named(&self.open_elems[self.open_elems.len() - 2], local_name!("optgroup")) { - self.pop(); + self.pop_unconditional(); } if self.current_node_named(local_name!("optgroup")) { - self.pop(); + self.pop_unconditional(); } else { self.unexpected(&token); } @@ -1132,7 +1141,7 @@ where => { if self.current_node_named(local_name!("option")) { - self.pop(); + self.pop_unconditional(); } else { self.unexpected(&token); } @@ -1289,7 +1298,7 @@ where if self.open_elems.len() == 1 { self.unexpected(&token); } else { - self.pop(); + self.pop_unconditional(); if !self.is_fragment() && !self.current_node_named(local_name!("frameset")) { self.mode = AfterFrameset; } diff --git a/markup5ever/Cargo.toml b/markup5ever/Cargo.toml index edcc0ba7..83bcc75e 100644 --- a/markup5ever/Cargo.toml +++ b/markup5ever/Cargo.toml @@ -22,3 +22,7 @@ log = "0.4" [build-dependencies] string_cache_codegen = "0.5.1" phf_codegen = "0.9" + +[features] +# Always use this feature in new code. In the future it will become the default. +api_v2 = [] diff --git a/markup5ever/interface/tree_builder.rs b/markup5ever/interface/tree_builder.rs index 43361f36..c9a68bca 100644 --- a/markup5ever/interface/tree_builder.rs +++ b/markup5ever/interface/tree_builder.rs @@ -11,6 +11,9 @@ //! //! It can be used by a parser to create the DOM graph structure in memory. +#[cfg(feature = "api_v2")] +use log::warn; + use crate::interface::{Attribute, ExpandedName, QualName}; use std::borrow::Cow; use tendril::StrTendril; @@ -75,6 +78,27 @@ pub struct ElementFlags { _private: (), } +#[derive(Debug)] +pub enum TreeBuilderError { + WronglyClosed(SuperfluousClosingElement), +} + +// TODO: Error message. +#[derive(Debug)] +pub struct SuperfluousClosingElement {} + +impl SuperfluousClosingElement { + pub fn new() -> Self { + Self { } + } +} + +impl From for TreeBuilderError { + fn from(value: SuperfluousClosingElement) -> Self { + Self::WronglyClosed(value) + } +} + /// A constructor for an element. /// /// # Examples @@ -185,8 +209,38 @@ pub trait TreeSink { fn mark_script_already_started(&mut self, _node: &Self::Handle) {} /// Indicate that a node was popped off the stack of open elements. + #[cfg(feature = "api_v2")] + fn pop(&mut self, node: &Self::Handle) -> Result<(), SuperfluousClosingElement> { + self.pop_v2(node) + } + + /// Indicate that a node was popped off the stack of open elements. + #[cfg(not(feature = "api_v2"))] + #[deprecated(note = "You are using an outdated API. Please use api_v2 feature.")] fn pop(&mut self, _node: &Self::Handle) {} + #[cfg(feature = "api_v2")] + /// Like pop(), but in the case of no open element, just warn instead of returning an error. + fn pop_unconditional(&mut self, node: &Self::Handle) { + if self.pop_v2(node).is_err() { + warn!("no current element"); + }; + } + + #[cfg(not(feature = "api_v2"))] + /// Like pop(), but in the case of no open element, just warn instead of returning an error. + fn pop_unconditional(&mut self, node: &Self::Handle) { + #[allow(deprecated)] + self.pop(node) + } + + /// Indicate that a node was popped off the stack of open elements. + /// + /// Note: Don't use this function, use pop() with api_v2 feature instead. + fn pop_v2(&mut self, _node: &Self::Handle) -> Result<(), SuperfluousClosingElement> { + Ok(()) + } + /// Get a handle to a template's template contents. The tree builder /// promises this will never be called with something else than /// a template element. diff --git a/rcdom/Cargo.toml b/rcdom/Cargo.toml index 5a5c0aed..193c6d23 100644 --- a/rcdom/Cargo.toml +++ b/rcdom/Cargo.toml @@ -39,3 +39,7 @@ harness = false [[test]] name = "xml-tokenizer" harness = false + +[features] +# Always use this feature in new code. In the future it will become the default. +api_v2 = [] diff --git a/xml5ever/Cargo.toml b/xml5ever/Cargo.toml index f20ea013..f77b21a3 100644 --- a/xml5ever/Cargo.toml +++ b/xml5ever/Cargo.toml @@ -28,3 +28,7 @@ criterion = "0.3" [[bench]] name = "xml5ever" harness = false + +[features] +# Always use this feature in new code. In the future it will become the default. +api_v2 = [] diff --git a/xml5ever/src/serialize/mod.rs b/xml5ever/src/serialize/mod.rs index 182ed9c8..84251a3b 100644 --- a/xml5ever/src/serialize/mod.rs +++ b/xml5ever/src/serialize/mod.rs @@ -60,7 +60,7 @@ impl NamespaceMapStack { } fn pop(&mut self) { - self.0.pop(); + self.0.pop().expect("no such element"); } } diff --git a/xml5ever/src/tree_builder/mod.rs b/xml5ever/src/tree_builder/mod.rs index 708776d0..3e09d164 100644 --- a/xml5ever/src/tree_builder/mod.rs +++ b/xml5ever/src/tree_builder/mod.rs @@ -11,7 +11,7 @@ mod types; use log::{debug, warn}; use mac::{_tt_as_expr_hack, matches, unwrap_or_return}; -use markup5ever::{local_name, namespace_prefix, namespace_url, ns}; +use markup5ever::{local_name, namespace_prefix, namespace_url, ns, interface::tree_builder::SuperfluousClosingElement}; use std::borrow::Cow; use std::borrow::Cow::Borrowed; use std::collections::btree_map::Iter; @@ -53,8 +53,8 @@ impl NamespaceMapStack { } #[doc(hidden)] - pub fn pop(&mut self) { - self.0.pop(); + pub fn pop_v2(&mut self) -> Option { + self.0.pop() } } @@ -428,7 +428,7 @@ where fn end(&mut self) { for node in self.open_elems.drain(..).rev() { - self.sink.pop(&node); + self.sink.pop_unconditional(&node); // It may give multiple warnings. Maybe better to produce just a single warning. } } @@ -437,8 +437,29 @@ where } } -fn current_node(open_elems: &[Handle]) -> &Handle { - open_elems.last().expect("no current element") +/// Indicate that a node was popped off the stack of open elements. +// #[cfg(feature = "api_v2")] +// fn current_node(open_elems: &[Handle]) -> Option<&Handle> { +// current_node_v2(open_elems) +// } + +// /// Indicate that a node was popped off the stack of open elements. +// #[cfg(not(feature = "api_v2"))] +// #[deprecated(note = "You are using an outdated API. Please use api_v2 feature.")] +// fn current_node(open_elems: &[Handle]) -> &Handle { +// current_node_unconditional(open_elems) +// } + +/// Like pop(), but in the case of no open element, just warn instead of returning an error. +fn current_node_unconditional(open_elems: &[Handle]) -> &Handle { + current_node_v2(open_elems).expect("no current element") +} + +/// Indicate that a node was popped off the stack of open elements. +/// +/// Note: Don't use this function, use pop() with api_v2 feature instead. +fn current_node_v2(open_elems: &[Handle]) -> Option<&Handle> { + open_elems.last() } #[doc(hidden)] @@ -447,12 +468,17 @@ where Handle: Clone, Sink: TreeSink, { - fn current_node(&self) -> &Handle { - self.open_elems.last().expect("no current element") + #[doc(hidden)] + fn current_node_v2(&self) -> Option<&Handle> { + self.open_elems.last() + } + + fn current_node_unconditional(&self) -> &Handle { + self.current_node_v2().expect("no current element") } fn insert_appropriately(&mut self, child: NodeOrText) { - let target = current_node(&self.open_elems); + let target = current_node_unconditional(&self.open_elems); // FIXME: Is using `current_node_unconditional()` correct? self.sink.append(target, child); } @@ -465,7 +491,7 @@ where fn append_tag(&mut self, tag: Tag) -> XmlProcessResult { let child = create_element(&mut self.sink, tag.name, tag.attrs); self.insert_appropriately(AppendNode(child.clone())); - self.sink.pop(&child); + self.sink.pop_unconditional(&child); // FIXME: This may be an error: Does the element necessarily exist? Done } @@ -490,7 +516,7 @@ where } fn append_comment_to_tag(&mut self, text: StrTendril) -> XmlProcessResult { - let target = current_node(&self.open_elems); + let target = current_node_unconditional(&self.open_elems); // FIXME: Is using `current_node_unconditional()` correct? let comment = self.sink.create_comment(text); self.sink.append(target, AppendNode(comment)); Done @@ -518,7 +544,7 @@ where } fn append_pi_to_tag(&mut self, pi: Pi) -> XmlProcessResult { - let target = current_node(&self.open_elems); + let target = current_node_unconditional(&self.open_elems); // FIXME: Is using `current_node_unconditional()` correct? let pi = self.sink.create_pi(pi.target, pi.data); self.sink.append(target, AppendNode(pi)); Done @@ -545,26 +571,27 @@ where if self.current_node_in(|x| pred(x)) { break; } - self.pop(); + self.pop_unconditional(); } } + // FIXME: Is using `current_node_unconditional()` correct? fn current_node_in(&self, set: TagSet) -> bool where TagSet: Fn(ExpandedName) -> bool, { // FIXME: take namespace into consideration: - set(self.sink.elem_name(self.current_node())) + set(self.sink.elem_name(self.current_node_unconditional())) } fn close_tag(&mut self, tag: Tag) -> XmlProcessResult { debug!( "Close tag: current_node.name {:?} \n Current tag {:?}", - self.sink.elem_name(self.current_node()), + self.sink.elem_name(self.current_node_unconditional()), &tag.name ); - if *self.sink.elem_name(self.current_node()).local != tag.name.local { + if *self.sink.elem_name(self.current_node_unconditional()).local != tag.name.local { self.sink .parse_error(Borrowed("Current node doesn't match tag")); } @@ -573,7 +600,7 @@ where if is_closed { self.pop_until(|p| p == tag.name.expanded()); - self.pop(); + self.pop_unconditional(); // FIXME: May this erroneously panic? } Done @@ -583,11 +610,44 @@ where self.open_elems.is_empty() } + // #[cfg(feature = "api_v2")] + // fn pop(&mut self) -> Result { + // self.pop_v2() + // } + + #[cfg(not(feature = "api_v2"))] fn pop(&mut self) -> Handle { - self.namespace_stack.pop(); - let node = self.open_elems.pop().expect("no current element"); - self.sink.pop(&node); - node + if let Ok(result) = self.pop_v2() { + result + } else { + panic!("no current element"); + } + } + + /// Like pop(), but in the case of no open element, just warn instead of returning an error. + #[cfg(feature = "api_v2")] + fn pop_unconditional(&mut self) { + if self.pop_v2().is_err() { + warn!("no current element"); + } + } + + #[cfg(not(feature = "api_v2"))] + fn pop_unconditional(&mut self) -> Handle { + self.pop() + } + + #[doc(hidden)] + fn pop_v2(&mut self) -> Result { + if self.namespace_stack.pop_v2().is_none() { + return Err(SuperfluousClosingElement::new()) + } + if let Some(node) = self.open_elems.pop() { + self.sink.pop_v2(&node)?; + Ok(node) + } else { + Err(SuperfluousClosingElement::new()) + } } fn stop_parsing(&mut self) -> XmlProcessResult { @@ -595,8 +655,9 @@ where Done } + // FIMXE: Is using `current_node_unconditional()` correct? fn complete_script(&mut self) { - let current = current_node(&self.open_elems); + let current = current_node_unconditional(&self.open_elems); if self.sink.complete_script(current) == NextParserState::Suspend { self.next_tokenizer_state = Some(Quiescent); } @@ -653,7 +714,7 @@ where }; self.phase = EndPhase; let handle = self.append_tag_to_doc(tag); - self.sink.pop(&handle); + self.sink.pop_v2(&handle).unwrap(); Done }, CommentToken(comment) => self.append_comment_to_doc(comment), @@ -738,7 +799,7 @@ where retval }, TagToken(Tag { kind: ShortTag, .. }) => { - self.pop(); + self.pop_unconditional(); // FIXME: May this erroneously panic? if self.no_open_elems() { self.phase = EndPhase; }