use std::{ borrow::Cow, cell::{Cell, Ref, RefCell}, collections::{HashSet, VecDeque}, rc::Rc, }; use html5ever::{ Attribute, LocalName, Namespace, QualName, tendril::StrTendril, tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}, }; use url::Url; use crate::script_planning::{PreparedImportMap, PreparedScript}; use moli_dom::native::Node; use moli_dom::native::{ Attribute as NativeAttribute, DomHost, NativeDom, NativeDomNodes, NativeNodeId, }; use moli_page_types::{ScriptMode, ScriptRun}; use moli_stylesheet_blocking::{ DocumentBlockingStylesheetSignature, DocumentOwnedBlockingStylesheetDiscoveryInput, }; use super::live_target::new_live_fragment_root_html_tree_sink_stream; use super::live_target::{ ParserDomMutationConsumer, ParserDomReadConsumer, ParserElementCreationConsumer, ParserMutationEffectConsumer, ParserMutationEffectDelivery, ParserRuntimeDomSinks, ParserStreamHtmlTreeSinkTarget, new_live_document_root_html_tree_sink_stream, new_parser_stream_html_tree_sink_stream, new_parser_stream_html_tree_sink_target, }; use super::{ ParserSourcePosition, html_chunks, session::{HtmlParserSession, html_parse_opts_with_scripting}, stream::HtmlTreeSinkStream, }; /// HTML parser configured with the scripting state of the target `Document`. /// /// This state controls HTML parsing semantics such as `", "", ); let disabled = parse_test_document_with_scripting(html, false); assert_eq!( disabled .elements_by_tag_name_ns(disabled.document_node_id(), Some(HTML_NS), "link", true,) .len(), 1, "head noscript content must be parsed as markup when scripting is disabled" ); assert_eq!( disabled .elements_by_tag_name_ns(disabled.document_node_id(), Some(HTML_NS), "main", true,) .len(), 1, "body noscript content must be parsed as markup when scripting is disabled" ); let enabled = parse_test_document_with_scripting(html, true); assert!( enabled .elements_by_tag_name_ns(enabled.document_node_id(), Some(HTML_NS), "link", true,) .is_empty(), "head noscript markup must remain inert when scripting is enabled" ); assert!( enabled .elements_by_tag_name_ns(enabled.document_node_id(), Some(HTML_NS), "main", true,) .is_empty(), "body noscript markup must remain raw text when scripting is enabled" ); } #[test] fn html4_empty_system_identifiers_trigger_quirks_mode() { let mode = |html: &str| { parse_test_document(html) .document() .expect("parsed document") .quirks_mode() }; let quirks = mode(""); let no_quirks = mode(""); let limited_quirks = mode(concat!( "", )); assert_ne!(quirks, no_quirks, "test must distinguish quirks mode"); assert_ne!( quirks, limited_quirks, "test must distinguish quirks from limited-quirks mode" ); assert_ne!( no_quirks, limited_quirks, "test must distinguish no-quirks from limited-quirks mode" ); for public_id in [ "-//W3C//DTD HTML 4.01 Frameset//", "-//w3c//dtd html 4.01 transitional//", ] { let missing = format!(""); let empty = format!(""); let nonempty = format!(""); assert_eq!(mode(&missing), quirks, "missing system ID for {public_id}"); assert_eq!(mode(&empty), quirks, "empty system ID for {public_id}"); assert_eq!( mode(&nonempty), limited_quirks, "nonempty system ID for {public_id}" ); } assert_eq!( mode(concat!( "", )), limited_quirks, "an empty XHTML system ID must remain limited-quirks" ); } #[test] fn fragment_scripting_flag_controls_noscript_tokenization() { let parse = |scripting_enabled| { HtmlParser::with_scripting_enabled(scripting_enabled) .parse_fragment_without_declarative_shadow_roots( Url::parse("https://example.test/").expect("test url"), HTML_NS, "body", "".to_owned(), ) }; let disabled = parse(false); assert_eq!( disabled .elements_by_tag_name_ns(disabled.document_node_id(), Some(HTML_NS), "span", true,) .len(), 1, "a scripting-disabled fragment must parse noscript children as markup" ); let enabled = parse(true); assert!( enabled .elements_by_tag_name_ns(enabled.document_node_id(), Some(HTML_NS), "span", true,) .is_empty(), "a scripting-enabled fragment must keep noscript children as text" ); } #[test] fn template_contents_do_not_publish_document_stylesheet_candidates() { let document = HtmlParser::SCRIPTING_ENABLED.parse_dom_host( Url::parse("https://template-styles.test/page.html").expect("test URL"), concat!( "", "", ) .to_owned(), ); assert!( document .stylesheet_candidate_handles_for_tree_scope(document.document_node_id()) .is_empty(), "inert template contents must not become Document stylesheet candidates" ); } #[test] fn mathml_annotation_xml_text_html_is_html_integration_point() { let document = parse_test_document(concat!( "", "
" )); let annotation = first_element_by_ns(&document, MATHML_NS, "annotation-xml"); let div = first_element_by_ns(&document, HTML_NS, "div"); assert_eq!( document.node(div).and_then(|node| node.parent_node_id()), Some(annotation), "HTML integration point children should remain under annotation-xml" ); } #[test] fn mathml_annotation_xml_without_html_encoding_is_not_integration_point() { let document = parse_test_document(concat!( "", "

" )); let annotation = first_element_by_ns(&document, MATHML_NS, "annotation-xml"); let paragraph = first_element_by_ns(&document, HTML_NS, "p"); assert_ne!( document .node(paragraph) .and_then(|node| node.parent_node_id()), Some(annotation), "plain annotation-xml must not opt into HTML integration point parsing" ); } #[test] fn customizable_select_preserves_option_wrapper_elements() { let document = parse_test_document(concat!( "", "" )); let select = first_element_by_ns(&document, HTML_NS, "select"); let divs = document.elements_by_tag_name_ns( document.document_node_id(), Some(HTML_NS), "div", true, ); let wrapper = divs .iter() .copied() .find(|handle| document.get_attribute(*handle, "id").as_deref() == Some("wrapper")) .expect("select wrapper div"); let nested = divs .iter() .copied() .find(|handle| document.get_attribute(*handle, "id").as_deref() == Some("nested")) .expect("nested select wrapper div"); assert_eq!( document .node(wrapper) .and_then(|node| node.parent_node_id()), Some(select) ); assert_eq!( document.node(nested).and_then(|node| node.parent_node_id()), Some(wrapper) ); assert_eq!(document.select_option_elements(select).len(), 3); let fragment = HtmlParser::SCRIPTING_ENABLED .parse_fragment_without_declarative_shadow_roots( Url::parse("https://example.test/").expect("test url"), HTML_NS, "select", "
".to_owned(), ); let fragment_wrapper = first_element_by_ns(&fragment, HTML_NS, "div"); let fragment_option = first_element_by_ns(&fragment, HTML_NS, "option"); assert_eq!( fragment .node(fragment_option) .and_then(|node| node.parent_node_id()), Some(fragment_wrapper), "select innerHTML parsing should preserve option wrapper elements" ); } #[test] fn decoded_parser_continues_after_meta_encoding_indicator() { let document = parse_test_document(concat!( "", "
" )); let div = first_element_by_ns(&document, HTML_NS, "div"); assert_eq!( document.get_attribute(div, "id").as_deref(), Some("after-meta") ); } #[test] fn parser_input_session_keeps_nested_pending_buffers_on_a_stack() { let queue = ParserInputQueue::default(); let session = queue.session(); let outer = session.enter_pending_context(); session.set_current_script_input_html("inner".to_owned()); assert_eq!(session.take_current_script_input_html(), "
inner"); session.set_current_script_input_html("
inner".to_owned()); drop(inner); assert_eq!( queue.take_next_script_input().as_deref(), Some("
inner") ); assert_eq!(outer.session().take_current_script_input_html(), "outer".to_owned()); drop(outer); assert_eq!( queue.take_next_script_input().as_deref(), Some("") ); } #[test] fn parser_input_session_keeps_insertion_preload_html_separate_from_script_input() { let queue = ParserInputQueue::default(); let session = queue.session(); session.enqueue_script_input_html("".to_owned()); session.enqueue_script_input_preload_html("".to_owned()); assert_eq!( queue.take_next_insertion_preload_input().as_deref(), Some("") ); assert_eq!( queue.take_next_script_input().as_deref(), Some("") ); } #[test] fn parser_input_session_transfers_insertion_meta_csp_acknowledgements_once() { let queue = ParserInputQueue::default(); let session = queue.session(); session.note_processed_insertion_meta_csp(1); session.note_processed_insertion_meta_csp(2); assert_eq!(queue.take_processed_insertion_meta_csp_count(), 3); assert_eq!(queue.take_processed_insertion_meta_csp_count(), 0); } }