mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
2190 lines
72 KiB
Rust
2190 lines
72 KiB
Rust
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,
|
|
};
|
|
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 `<noscript>` tokenization;
|
|
/// it does not execute JavaScript. Callers must choose the state explicitly so
|
|
/// detached and sandboxed documents cannot accidentally inherit the normal
|
|
/// browsing-context default.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct HtmlParser {
|
|
scripting_enabled: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ParserInputSession(Rc<RefCell<ParserInputState>>);
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParserInputContext {
|
|
session: ParserInputSession,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParserInputQueue(Rc<RefCell<ParserInputState>>);
|
|
|
|
#[derive(Debug, Default)]
|
|
struct ParserInputState {
|
|
script_input_queue: VecDeque<String>,
|
|
insertion_preload_queue: VecDeque<String>,
|
|
pending_stack: Vec<String>,
|
|
processed_insertion_meta_csp_count: usize,
|
|
}
|
|
|
|
pub struct DocumentStream {
|
|
inner: HtmlTreeSinkStream,
|
|
input: RefCell<HtmlParserInputStream>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct HtmlParserInputStream {
|
|
end_segments: VecDeque<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParserStreamDocumentSnapshot(NativeDom);
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParserPumpOutcome {
|
|
pub result: ParserPumpStep,
|
|
pub discovered_async_prefetch_scripts: Vec<PreparedScript>,
|
|
pub discovered_modulepreload_link_candidates: Vec<NativeNodeId>,
|
|
pub discovered_blocking_stylesheet_inputs: Vec<DocumentOwnedBlockingStylesheetDiscoveryInput>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct ParserFinishDiscoverySignals {
|
|
pub parser_created_null_registry_elements: Vec<NativeNodeId>,
|
|
pub discovered_modulepreload_link_candidates: Vec<NativeNodeId>,
|
|
pub discovered_parser_meta_csp_candidates: Vec<NativeNodeId>,
|
|
pub discovered_blocking_stylesheet_inputs: Vec<DocumentOwnedBlockingStylesheetDiscoveryInput>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ParserScriptHandoff {
|
|
BlockingClassic {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
blocking_signatures_before: HashSet<DocumentBlockingStylesheetSignature>,
|
|
script: PreparedScript,
|
|
},
|
|
AsyncPostParse {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
script: PreparedScript,
|
|
},
|
|
NonAsyncPostParse {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
blocking_signatures_before: HashSet<DocumentBlockingStylesheetSignature>,
|
|
script: PreparedScript,
|
|
},
|
|
ImportMap {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
import_map: PreparedImportMap,
|
|
},
|
|
NoExecution {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
outcome: ParserScriptNoExecutionOutcome,
|
|
},
|
|
PreparationFailure {
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
failure: ParserScriptPreparationFailure,
|
|
},
|
|
}
|
|
|
|
impl ParserScriptHandoff {
|
|
pub fn node_id(&self) -> NativeNodeId {
|
|
self.start_position().0
|
|
}
|
|
|
|
pub fn start_position(&self) -> (NativeNodeId, u64, u64) {
|
|
match self {
|
|
Self::BlockingClassic {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
}
|
|
| Self::AsyncPostParse {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
}
|
|
| Self::NonAsyncPostParse {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
}
|
|
| Self::ImportMap {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
}
|
|
| Self::NoExecution {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
}
|
|
| Self::PreparationFailure {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} => (*node_id, *start_line, *start_column),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParserScriptNoExecutionOutcome {
|
|
position: usize,
|
|
mode: ScriptMode,
|
|
run: Option<ScriptRun>,
|
|
element_state_transition: ParserScriptElementStateTransition,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParserScriptPreparationFailure {
|
|
position: usize,
|
|
mode: ScriptMode,
|
|
message: String,
|
|
element_state_transition: ParserScriptElementStateTransition,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
pub enum ParserScriptElementStateTransition {
|
|
#[default]
|
|
None,
|
|
ConsumeParserInserted {
|
|
force_async: bool,
|
|
},
|
|
MarkAlreadyStarted,
|
|
}
|
|
|
|
impl ParserScriptNoExecutionOutcome {
|
|
pub fn ignored(position: usize, mode: ScriptMode) -> Self {
|
|
Self {
|
|
position,
|
|
mode,
|
|
run: None,
|
|
element_state_transition: ParserScriptElementStateTransition::None,
|
|
}
|
|
}
|
|
|
|
pub fn skipped(position: usize, mode: ScriptMode, run: ScriptRun) -> Self {
|
|
Self {
|
|
position,
|
|
mode,
|
|
run: Some(run),
|
|
element_state_transition: ParserScriptElementStateTransition::None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn with_element_state_transition(
|
|
mut self,
|
|
transition: ParserScriptElementStateTransition,
|
|
) -> Self {
|
|
self.element_state_transition = transition;
|
|
self
|
|
}
|
|
|
|
pub fn position(&self) -> usize {
|
|
self.position
|
|
}
|
|
|
|
pub fn mode(&self) -> ScriptMode {
|
|
self.mode
|
|
}
|
|
|
|
pub fn run(&self) -> Option<&ScriptRun> {
|
|
self.run.as_ref()
|
|
}
|
|
|
|
pub fn element_state_transition(&self) -> ParserScriptElementStateTransition {
|
|
self.element_state_transition
|
|
}
|
|
|
|
pub fn into_parts(self) -> (usize, ScriptMode, Option<ScriptRun>) {
|
|
(self.position, self.mode, self.run)
|
|
}
|
|
}
|
|
|
|
impl ParserScriptPreparationFailure {
|
|
pub fn new(position: usize, mode: ScriptMode, message: String) -> Self {
|
|
Self {
|
|
position,
|
|
mode,
|
|
message,
|
|
element_state_transition: ParserScriptElementStateTransition::None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn with_element_state_transition(
|
|
mut self,
|
|
transition: ParserScriptElementStateTransition,
|
|
) -> Self {
|
|
self.element_state_transition = transition;
|
|
self
|
|
}
|
|
|
|
pub fn position(&self) -> usize {
|
|
self.position
|
|
}
|
|
|
|
pub fn mode(&self) -> ScriptMode {
|
|
self.mode
|
|
}
|
|
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
|
|
pub fn element_state_transition(&self) -> ParserScriptElementStateTransition {
|
|
self.element_state_transition
|
|
}
|
|
|
|
pub fn into_parts(self) -> (usize, ScriptMode, String) {
|
|
(self.position, self.mode, self.message)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParserCustomElementConstructionHandoff {
|
|
pub placeholder: NativeNodeId,
|
|
pub local_name: String,
|
|
pub namespace: String,
|
|
pub prefix: Option<String>,
|
|
pub attributes: Vec<NativeAttribute>,
|
|
pub owner_document: NativeNodeId,
|
|
pub parent_at_creation: Option<NativeNodeId>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ParserBlockingStylesheetPause {
|
|
pub node_id: NativeNodeId,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ParserYield {
|
|
/// A synchronous callback suspended or canceled this parser. The owner
|
|
/// already holds the blocker; this yield transfers no new work.
|
|
OwnerInterrupted,
|
|
Script(Box<ParserScriptHandoff>),
|
|
CustomElementConstruction(Box<ParserCustomElementConstructionHandoff>),
|
|
BlockingStylesheet(ParserBlockingStylesheetPause),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ParserPumpStep {
|
|
Yield(ParserYield),
|
|
InputDrained,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(super) struct ParseHandle {
|
|
identity: ParseHandleIdentity,
|
|
pub(super) element_name: Option<Rc<QualName>>,
|
|
pub(super) parser_flags: ParserElementFlags,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum ParseHandleIdentity {
|
|
DomNode(NativeNodeId),
|
|
// Standalone fragment parsing, like `Element.innerHTML` staging, only has a
|
|
// context element name. Chromium keeps a real `context_element` next to the
|
|
// `DocumentFragment` target; our detached staging parser uses this
|
|
// parser-only handle instead of pretending the document node is that
|
|
// context. It may answer parser questions such as `elem_name`, but it must
|
|
// never be used as a DOM mutation target.
|
|
SyntheticFragmentContext,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub(super) struct ParserElementFlags {
|
|
// html5ever passes this through ElementFlags and later asks the TreeSink for
|
|
// it with the same parser handle while deciding whether MathML
|
|
// annotation-xml children should switch back to HTML insertion rules.
|
|
pub(super) mathml_annotation_xml_integration_point: bool,
|
|
}
|
|
|
|
impl ParserElementFlags {
|
|
pub(super) fn from_html5ever(flags: &ElementFlags) -> Self {
|
|
// Keep only parser-handle metadata here. `flags.template` creates
|
|
// DOM-observable template contents and stays in the template handling
|
|
// path instead of becoming parser-only state.
|
|
Self {
|
|
mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) struct DocumentSink {
|
|
target: RefCell<ParserStreamHtmlTreeSinkTarget>,
|
|
// html5ever exposes token lines but not columns. Once inserted input is
|
|
// mixed into the tokenizer queue, it also cannot distinguish generated
|
|
// lines from the original document tail, so location fidelity only
|
|
// degrades and never recovers for this parser session.
|
|
source_positions_known: Cell<bool>,
|
|
html4_empty_system_id_quirks_override_pending: Cell<bool>,
|
|
}
|
|
|
|
const HTML4_QUIRKS_PUBLIC_ID_PREFIXES: [&str; 2] = [
|
|
"-//W3C//DTD HTML 4.01 Frameset//",
|
|
"-//W3C//DTD HTML 4.01 Transitional//",
|
|
];
|
|
|
|
fn html4_doctype_without_nonempty_system_id_requires_quirks(
|
|
name: &str,
|
|
public_id: &str,
|
|
system_id: &str,
|
|
) -> bool {
|
|
name.eq_ignore_ascii_case("html")
|
|
&& system_id.is_empty()
|
|
&& HTML4_QUIRKS_PUBLIC_ID_PREFIXES.iter().any(|prefix| {
|
|
public_id
|
|
.get(..prefix.len())
|
|
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
|
|
})
|
|
}
|
|
|
|
impl HtmlParser {
|
|
/// Convenience policy for tests that model an executable `Document`.
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub const SCRIPTING_ENABLED: Self = Self::with_scripting_enabled(true);
|
|
|
|
/// Convenience policy for tests that model a non-executable `Document`.
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub const SCRIPTING_DISABLED: Self = Self::with_scripting_enabled(false);
|
|
|
|
pub const fn with_scripting_enabled(scripting_enabled: bool) -> Self {
|
|
Self { scripting_enabled }
|
|
}
|
|
|
|
pub fn parse(&self, final_url: Url, html: String) -> NativeDom {
|
|
self.parse_dom_host(final_url, html).into_dom()
|
|
}
|
|
|
|
pub fn parse_dom_host(&self, final_url: Url, html: String) -> DomHost {
|
|
let stream = self.start_document(final_url);
|
|
for chunk in html_chunks(&html) {
|
|
stream.feed(chunk);
|
|
}
|
|
stream.finish_dom_host()
|
|
}
|
|
|
|
pub fn parse_without_declarative_shadow_roots(
|
|
&self,
|
|
final_url: Url,
|
|
html: String,
|
|
) -> NativeDom {
|
|
let target =
|
|
ParserStreamHtmlTreeSinkTarget::new_with_declarative_shadow_roots(final_url, false);
|
|
let stream = HtmlTreeSinkStream::from_target_with_scripting(target, self.scripting_enabled);
|
|
for chunk in html_chunks(&html) {
|
|
stream.feed(chunk);
|
|
}
|
|
stream.finish()
|
|
}
|
|
|
|
pub fn start_document(&self, final_url: Url) -> DocumentStream {
|
|
DocumentStream::new_parser_stream(final_url, self.scripting_enabled)
|
|
}
|
|
|
|
pub fn start_live_document_root(
|
|
&self,
|
|
final_url: Url,
|
|
document_handle: NativeNodeId,
|
|
) -> DocumentStream {
|
|
DocumentStream::new_live_document_root(final_url, document_handle, self.scripting_enabled)
|
|
}
|
|
|
|
pub fn parse_fragment_without_declarative_shadow_roots(
|
|
&self,
|
|
final_url: Url,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
html: String,
|
|
) -> NativeDom {
|
|
self.parse_fragment_with_declarative_shadow_roots(
|
|
final_url,
|
|
context_namespace,
|
|
context_local_name,
|
|
html,
|
|
false,
|
|
self.scripting_enabled,
|
|
)
|
|
}
|
|
|
|
fn parse_fragment_with_declarative_shadow_roots(
|
|
&self,
|
|
final_url: Url,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
html: String,
|
|
allow_declarative_shadow_roots: bool,
|
|
scripting_enabled: bool,
|
|
) -> NativeDom {
|
|
let target = ParserStreamHtmlTreeSinkTarget::new_with_declarative_shadow_roots(
|
|
final_url,
|
|
allow_declarative_shadow_roots,
|
|
);
|
|
let context = QualName::new(
|
|
None,
|
|
Namespace::from(context_namespace),
|
|
LocalName::from(context_local_name),
|
|
);
|
|
let context_handle = ParseHandle::new_synthetic_fragment_context(Rc::new(context));
|
|
let sink = DocumentSink::new(target);
|
|
let parser = HtmlParserSession::new_fragment(
|
|
sink,
|
|
html_parse_opts_with_scripting(scripting_enabled),
|
|
context_handle,
|
|
scripting_enabled,
|
|
);
|
|
|
|
for chunk in html_chunks(&html) {
|
|
parser.process(StrTendril::from(chunk));
|
|
}
|
|
parser.finish().finish_document(html)
|
|
}
|
|
|
|
/// Parses an inert HTML fragment directly into a runtime-owned detached
|
|
/// `DocumentFragment`.
|
|
///
|
|
/// The parser mutates only through the scoped consumer callbacks. Custom
|
|
/// elements are intentionally left unconstructed so the caller can apply
|
|
/// the fragment API's required upgrade timing after normalization or
|
|
/// insertion.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn parse_fragment_into_live_dom<T>(
|
|
&self,
|
|
final_url: Url,
|
|
fragment_handle: NativeNodeId,
|
|
owner_document_handle: NativeNodeId,
|
|
context_handle: NativeNodeId,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
html: &str,
|
|
consumer: &mut T,
|
|
allow_declarative_shadow_roots: bool,
|
|
) -> ParserFinishDiscoverySignals
|
|
where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
{
|
|
// TreeBuilder::new_for_fragment creates its synthetic root eagerly, so
|
|
// construction itself must run under the same scoped runtime sinks as
|
|
// every later feed/finish operation.
|
|
let runtime_dom_sinks =
|
|
// SAFETY: `consumer` remains exclusively borrowed until the
|
|
// constructor clears the sink bundle before returning.
|
|
unsafe { ParserRuntimeDomSinks::from_consumer_without_element_creation(consumer) };
|
|
let stream = DocumentStream::new_live_fragment_root(
|
|
final_url,
|
|
fragment_handle,
|
|
owner_document_handle,
|
|
context_handle,
|
|
context_namespace,
|
|
context_local_name,
|
|
runtime_dom_sinks,
|
|
allow_declarative_shadow_roots,
|
|
self.scripting_enabled,
|
|
);
|
|
for chunk in html_chunks(html) {
|
|
stream.feed_with_runtime_dom_consumer_without_element_creation(chunk, consumer);
|
|
}
|
|
stream.finish_with_runtime_dom_consumer_without_element_creation(consumer)
|
|
}
|
|
}
|
|
|
|
impl Default for ParserInputQueue {
|
|
fn default() -> Self {
|
|
Self(Rc::new(RefCell::new(ParserInputState::default())))
|
|
}
|
|
}
|
|
|
|
impl DocumentStream {
|
|
fn new_parser_stream(final_url: Url, scripting_enabled: bool) -> Self {
|
|
Self {
|
|
inner: new_parser_stream_html_tree_sink_stream(final_url, scripting_enabled),
|
|
input: RefCell::default(),
|
|
}
|
|
}
|
|
|
|
fn new_live_document_root(
|
|
final_url: Url,
|
|
document_handle: NativeNodeId,
|
|
scripting_enabled: bool,
|
|
) -> Self {
|
|
Self {
|
|
inner: new_live_document_root_html_tree_sink_stream(
|
|
final_url,
|
|
document_handle,
|
|
scripting_enabled,
|
|
),
|
|
input: RefCell::default(),
|
|
}
|
|
}
|
|
|
|
fn new_live_fragment_root(
|
|
final_url: Url,
|
|
fragment_handle: NativeNodeId,
|
|
owner_document_handle: NativeNodeId,
|
|
context_handle: NativeNodeId,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
runtime_dom_sinks: ParserRuntimeDomSinks,
|
|
allow_declarative_shadow_roots: bool,
|
|
scripting_enabled: bool,
|
|
) -> Self {
|
|
Self {
|
|
inner: new_live_fragment_root_html_tree_sink_stream(
|
|
final_url,
|
|
fragment_handle,
|
|
owner_document_handle,
|
|
context_handle,
|
|
context_namespace,
|
|
context_local_name,
|
|
runtime_dom_sinks,
|
|
allow_declarative_shadow_roots,
|
|
scripting_enabled,
|
|
),
|
|
input: RefCell::default(),
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn is_parser_stream_backend_for_testing(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn new_scripting_enabled_parser_stream_for_testing(final_url: Url) -> Self {
|
|
Self::new_parser_stream(final_url, true)
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn new_scripting_enabled_live_document_root_for_testing(
|
|
final_url: Url,
|
|
document_handle: NativeNodeId,
|
|
) -> Self {
|
|
Self::new_live_document_root(final_url, document_handle, true)
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn new_live_fragment_root_for_testing<T>(
|
|
final_url: Url,
|
|
fragment_handle: NativeNodeId,
|
|
owner_document_handle: NativeNodeId,
|
|
context_handle: NativeNodeId,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
consumer: &mut T,
|
|
allow_declarative_shadow_roots: bool,
|
|
scripting_enabled: bool,
|
|
) -> Self
|
|
where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
{
|
|
// SAFETY: this constructor consumes the erased callbacks while `consumer`
|
|
// is borrowed and clears them before returning the DocumentStream.
|
|
let runtime_dom_sinks =
|
|
unsafe { ParserRuntimeDomSinks::from_consumer_without_element_creation(consumer) };
|
|
Self::new_live_fragment_root(
|
|
final_url,
|
|
fragment_handle,
|
|
owner_document_handle,
|
|
context_handle,
|
|
context_namespace,
|
|
context_local_name,
|
|
runtime_dom_sinks,
|
|
allow_declarative_shadow_roots,
|
|
scripting_enabled,
|
|
)
|
|
}
|
|
|
|
pub fn note_defined_autonomous_custom_element(&self, local_name: &str) {
|
|
self.inner
|
|
.note_defined_autonomous_custom_element(local_name);
|
|
}
|
|
|
|
pub fn drain_pending_custom_element_construction_handoffs(
|
|
&self,
|
|
) -> Vec<ParserCustomElementConstructionHandoff> {
|
|
self.inner
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
}
|
|
|
|
pub fn script_input_session(&self) -> ParserInputSession {
|
|
self.inner.script_input_session()
|
|
}
|
|
|
|
pub fn take_next_script_input(&self) -> Option<String> {
|
|
self.inner.take_next_script_input()
|
|
}
|
|
|
|
pub fn has_script_input(&self) -> bool {
|
|
self.inner.has_script_input()
|
|
}
|
|
|
|
pub fn take_next_insertion_preload_input(&self) -> Option<String> {
|
|
self.inner.take_next_insertion_preload_input()
|
|
}
|
|
|
|
pub fn take_processed_insertion_meta_csp_count(&self) -> usize {
|
|
self.inner.take_processed_insertion_meta_csp_count()
|
|
}
|
|
|
|
pub fn feed(&self, chunk: &str) {
|
|
self.inner.feed(chunk)
|
|
}
|
|
|
|
fn feed_with_runtime_dom_consumer_without_element_creation<T>(
|
|
&self,
|
|
chunk: &str,
|
|
consumer: &mut T,
|
|
) where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call; the
|
|
// parser-step Drop guard removes every erased callback before return.
|
|
let sinks =
|
|
unsafe { ParserRuntimeDomSinks::from_consumer_without_element_creation(consumer) };
|
|
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
|
let step = RuntimeDomSinksParserStep { stream: self };
|
|
step.feed(chunk);
|
|
}
|
|
|
|
/// Append decoded document input to the parser-owned end segment chain.
|
|
///
|
|
/// The tokenizer only receives a bounded prefix when the owner pumps the
|
|
/// parser. Appending while a script or stylesheet blocks parsing therefore
|
|
/// cannot advance the DOM commit frontier.
|
|
pub fn append_to_end(&self, chunk: String) {
|
|
if !chunk.is_empty() {
|
|
self.input.borrow_mut().end_segments.push_back(chunk);
|
|
}
|
|
}
|
|
|
|
/// Append input at the end of the currently active parser-inserted frame.
|
|
///
|
|
/// This is used when a parser script has already inserted input and then
|
|
/// continues writing while that input is blocked on a nested resource.
|
|
/// The input must remain after the blocked frame's unconsumed tail rather
|
|
/// than becoming a newer nested insertion.
|
|
pub fn append_to_current_inserted_input(&self, chunk: &str) -> bool {
|
|
self.inner.append_to_current_inserted_input(chunk)
|
|
}
|
|
|
|
/// Insert at the active token's insertion point without opening a script
|
|
/// frame. A parser reaction appends after the current frame's unread tail.
|
|
pub fn append_at_current_insertion_point(&self, chunk: &str) {
|
|
self.inner.append_at_current_insertion_point(chunk);
|
|
}
|
|
|
|
pub fn has_pending_input(&self) -> bool {
|
|
self.inner.has_script_input()
|
|
|| self.inner.has_buffered_input()
|
|
|| !self.input.borrow().end_segments.is_empty()
|
|
}
|
|
|
|
pub fn next_input_len(&self) -> usize {
|
|
self.inner
|
|
.next_script_input_len()
|
|
.or_else(|| {
|
|
self.inner
|
|
.has_buffered_input()
|
|
.then(|| self.inner.buffered_input_len())
|
|
})
|
|
.or_else(|| self.input.borrow().end_segments.front().map(String::len))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn snapshot_pending_input(&self) -> String {
|
|
let mut pending = self.inner.snapshot_script_input();
|
|
pending.push_str(&self.inner.snapshot_buffered_input());
|
|
for segment in &self.input.borrow().end_segments {
|
|
pending.push_str(segment);
|
|
}
|
|
pending
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn queued_end_segment_count_for_testing(&self) -> usize {
|
|
self.input.borrow().end_segments.len()
|
|
}
|
|
|
|
fn take_next_owned_input(&self, max_bytes: usize) -> (String, bool) {
|
|
if let Some(input) = self.inner.take_next_script_input() {
|
|
// One parser insertion is an atomic source segment. Splitting it
|
|
// outside html5ever would let a remainder jump ahead of bytes the
|
|
// tokenizer retained when it yielded on a script boundary.
|
|
return (input, true);
|
|
}
|
|
|
|
if self.inner.has_buffered_input() {
|
|
return (String::new(), false);
|
|
}
|
|
|
|
let Some(input) = self.input.borrow_mut().end_segments.pop_front() else {
|
|
return (String::new(), false);
|
|
};
|
|
let (prefix, remainder) = split_parser_input_prefix(input, max_bytes);
|
|
if let Some(remainder) = remainder {
|
|
self.input.borrow_mut().end_segments.push_front(remainder);
|
|
}
|
|
(prefix, false)
|
|
}
|
|
|
|
pub fn pump_next_parser_step(&self, max_bytes: usize) -> ParserPumpOutcome {
|
|
let (chunk, inserted_source) = self.take_next_owned_input(max_bytes);
|
|
if inserted_source {
|
|
self.inner.pump_parser_inserted_step(&chunk)
|
|
} else {
|
|
self.inner.pump_parser_step(&chunk)
|
|
}
|
|
}
|
|
|
|
/// Feed parser input until either the current buffer is exhausted or the parser yields a
|
|
/// concrete embedder control-flow boundary.
|
|
///
|
|
/// This is the low-level surface the runtime needs for parser/script coordination:
|
|
/// instead of slicing the original HTML string on `</script>` boundaries and hoping the next
|
|
/// parser step will line up with script readiness, we drive html5ever one tokenizer run at a
|
|
/// time and stop when the tree builder returns `TokenizerResult::Script(handle)` or when
|
|
/// Moli-side parser state records another parser yield reason.
|
|
///
|
|
/// Important constraints:
|
|
/// - this method only exposes *when parser control should be yielded back to runtime*
|
|
/// - it does not execute JS itself
|
|
/// - any actual V8/isolate work still happens in the runtime coordination layer
|
|
///
|
|
/// Returning `ParserPumpStep::Yield(reason)` means the parser has stopped before consuming
|
|
/// following tokens. Script and custom-element yields transfer work to the runtime; a
|
|
/// blocking-stylesheet yield only asks the runtime to retain and later resume this parser.
|
|
/// No stylesheet ownership is transferred.
|
|
///
|
|
/// Returning `ParserPumpStep::InputDrained` means:
|
|
/// - the current input buffer has been consumed as far as html5ever can go for now
|
|
/// - either there is no pending yield, or more bytes are needed before another boundary exists
|
|
pub fn pump_parser_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
self.inner.pump_parser_step(chunk)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn pump_parser_inserted_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
self.inner.pump_parser_inserted_step(chunk)
|
|
}
|
|
|
|
pub fn pump_parser_step_with_runtime_dom_consumer<T>(
|
|
&self,
|
|
chunk: &str,
|
|
consumer: &mut T,
|
|
) -> ParserPumpOutcome
|
|
where
|
|
T: ParserDomReadConsumer
|
|
+ ParserDomMutationConsumer
|
|
+ ParserMutationEffectConsumer
|
|
+ ParserElementCreationConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call; the
|
|
// parser-step Drop guard removes every erased callback before return.
|
|
let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) };
|
|
self.pump_parser_step_with_runtime_dom_sinks(chunk, sinks)
|
|
}
|
|
|
|
pub fn pump_next_parser_step_with_runtime_dom_consumer<T>(
|
|
&self,
|
|
max_bytes: usize,
|
|
consumer: &mut T,
|
|
) -> ParserPumpOutcome
|
|
where
|
|
T: ParserDomReadConsumer
|
|
+ ParserDomMutationConsumer
|
|
+ ParserMutationEffectConsumer
|
|
+ ParserElementCreationConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call; the
|
|
// parser-step Drop guard removes every erased callback before return.
|
|
let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) };
|
|
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
|
let step = RuntimeDomSinksParserStep { stream: self };
|
|
step.pump_next_parser_step(max_bytes)
|
|
}
|
|
|
|
pub fn pump_parser_step_with_runtime_dom_consumer_without_element_creation<T>(
|
|
&self,
|
|
chunk: &str,
|
|
consumer: &mut T,
|
|
) -> ParserPumpOutcome
|
|
where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call; the
|
|
// parser-step Drop guard removes every erased callback before return.
|
|
let sinks =
|
|
unsafe { ParserRuntimeDomSinks::from_consumer_without_element_creation(consumer) };
|
|
self.pump_parser_step_with_runtime_dom_sinks(chunk, sinks)
|
|
}
|
|
|
|
pub fn pump_parser_step_with_runtime_dom_consumers<T, E>(
|
|
&self,
|
|
chunk: &str,
|
|
consumer: &mut T,
|
|
element_consumer: &mut E,
|
|
) -> ParserPumpOutcome
|
|
where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
E: ParserElementCreationConsumer,
|
|
{
|
|
// SAFETY: both consumers stay exclusively and independently borrowed
|
|
// until the parser-step Drop guard removes every erased callback.
|
|
let sinks = unsafe { ParserRuntimeDomSinks::from_consumers(consumer, element_consumer) };
|
|
self.pump_parser_step_with_runtime_dom_sinks(chunk, sinks)
|
|
}
|
|
|
|
fn pump_parser_step_with_runtime_dom_sinks(
|
|
&self,
|
|
chunk: &str,
|
|
sinks: ParserRuntimeDomSinks,
|
|
) -> ParserPumpOutcome {
|
|
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
|
let step = RuntimeDomSinksParserStep { stream: self };
|
|
step.pump_parser_step(chunk)
|
|
}
|
|
|
|
pub fn pump_parser_inserted_step_with_runtime_dom_consumer<T>(
|
|
&self,
|
|
chunk: &str,
|
|
consumer: &mut T,
|
|
) -> ParserPumpOutcome
|
|
where
|
|
T: ParserDomReadConsumer
|
|
+ ParserDomMutationConsumer
|
|
+ ParserMutationEffectConsumer
|
|
+ ParserElementCreationConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call; the
|
|
// parser-step Drop guard removes every erased callback before return.
|
|
let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) };
|
|
self.pump_parser_inserted_step_with_runtime_dom_sinks(chunk, sinks)
|
|
}
|
|
|
|
fn pump_parser_inserted_step_with_runtime_dom_sinks(
|
|
&self,
|
|
chunk: &str,
|
|
sinks: ParserRuntimeDomSinks,
|
|
) -> ParserPumpOutcome {
|
|
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
|
let step = RuntimeDomSinksParserStep { stream: self };
|
|
step.pump_parser_inserted_step(chunk)
|
|
}
|
|
|
|
pub fn finish_with_runtime_dom_consumer<T>(
|
|
self,
|
|
consumer: &mut T,
|
|
) -> ParserFinishDiscoverySignals
|
|
where
|
|
T: ParserDomReadConsumer
|
|
+ ParserDomMutationConsumer
|
|
+ ParserMutationEffectConsumer
|
|
+ ParserElementCreationConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call. A normal
|
|
// finish consumes the parser and its bundle; the Drop guard clears the
|
|
// bundle if finish unwinds before consumption.
|
|
let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) };
|
|
self.finish_with_runtime_dom_sinks(sinks)
|
|
}
|
|
|
|
fn finish_with_runtime_dom_consumer_without_element_creation<T>(
|
|
self,
|
|
consumer: &mut T,
|
|
) -> ParserFinishDiscoverySignals
|
|
where
|
|
T: ParserDomReadConsumer + ParserDomMutationConsumer + ParserMutationEffectConsumer,
|
|
{
|
|
// SAFETY: `consumer` stays exclusively borrowed for this call. The
|
|
// finish guard clears the callbacks if parser finalization unwinds.
|
|
let sinks =
|
|
unsafe { ParserRuntimeDomSinks::from_consumer_without_element_creation(consumer) };
|
|
self.finish_with_runtime_dom_sinks(sinks)
|
|
}
|
|
|
|
fn finish_with_runtime_dom_sinks(
|
|
self,
|
|
sinks: ParserRuntimeDomSinks,
|
|
) -> ParserFinishDiscoverySignals {
|
|
self.inner.enter_runtime_dom_sinks_parse_step(sinks);
|
|
let mut finish = RuntimeDomSinksParserFinish { stream: Some(self) };
|
|
finish.finish()
|
|
}
|
|
|
|
pub fn with_stylesheet_blocking_read_view<R>(
|
|
&self,
|
|
f: impl FnOnce(&dyn moli_stylesheet_blocking::StylesheetBlockingReadView) -> R,
|
|
) -> R {
|
|
self.inner.with_stylesheet_blocking_read_view(f)
|
|
}
|
|
|
|
pub fn snapshot_parser_stream_document(&self) -> ParserStreamDocumentSnapshot {
|
|
ParserStreamDocumentSnapshot(self.inner.snapshot_parser_stream_document())
|
|
}
|
|
|
|
pub fn snapshot_parser_stream_dom_host(&self) -> DomHost {
|
|
self.inner.snapshot_parser_stream_dom_host()
|
|
}
|
|
|
|
pub fn take_parser_stream_null_custom_element_registry_elements(&self) -> Vec<NativeNodeId> {
|
|
self.inner
|
|
.take_parser_stream_null_custom_element_registry_elements()
|
|
}
|
|
|
|
pub fn take_parser_stream_dom_host(&self) -> DomHost {
|
|
self.inner.take_parser_stream_dom_host()
|
|
}
|
|
|
|
pub fn restore_parser_stream_dom_host(&self, dom_host: DomHost) {
|
|
self.inner.restore_parser_stream_dom_host(dom_host);
|
|
}
|
|
|
|
pub fn with_parser_stream_dom_host_for_bootstrap<R>(
|
|
&self,
|
|
f: impl FnOnce(DomHost) -> std::result::Result<R, Box<(anyhow::Error, DomHost)>>,
|
|
) -> anyhow::Result<R> {
|
|
let bootstrap_document = self.inner.take_parser_stream_dom_host();
|
|
match f(bootstrap_document) {
|
|
Ok(result) => Ok(result),
|
|
Err(error) => {
|
|
let (error, bootstrap_document) = *error;
|
|
self.inner
|
|
.restore_parser_stream_dom_host(bootstrap_document);
|
|
Err(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn replace_parser_stream_document_from_snapshot(
|
|
&self,
|
|
document: ParserStreamDocumentSnapshot,
|
|
) {
|
|
self.inner.replace_parser_stream_document(document.into())
|
|
}
|
|
|
|
pub fn drain_ready_parser_scripts(&self) -> Vec<NativeNodeId> {
|
|
self.inner.drain_ready_parser_scripts()
|
|
}
|
|
|
|
pub fn drain_discovered_async_prefetch_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.inner.drain_discovered_async_prefetch_candidates()
|
|
}
|
|
|
|
pub fn drain_discovered_modulepreload_link_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.inner.drain_discovered_modulepreload_link_candidates()
|
|
}
|
|
|
|
pub fn drain_discovered_parser_meta_csp_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.inner.drain_discovered_parser_meta_csp_candidates()
|
|
}
|
|
|
|
pub fn mark_script_already_started(&self, node_id: NativeNodeId) {
|
|
// The streaming parser and the live runtime intentionally share DOM snapshots during
|
|
// parse-time execution. When runtime code claims ownership of a parser-discovered script
|
|
// without executing it immediately (phase 2 `defer` / external `async`), we still need
|
|
// the parser-side DOM to remember that claim. Otherwise `finish()` would hand back a
|
|
// snapshot where the same script still looks fresh, and the later whole-document planner
|
|
// would rediscover and execute it a second time.
|
|
self.inner.mark_script_already_started(node_id)
|
|
}
|
|
|
|
pub fn finish(self) -> NativeDom {
|
|
self.inner.finish()
|
|
}
|
|
|
|
pub fn finish_dom_host(self) -> DomHost {
|
|
self.inner.finish_dom_host()
|
|
}
|
|
}
|
|
|
|
struct RuntimeDomSinksParserStep<'a> {
|
|
stream: &'a DocumentStream,
|
|
}
|
|
|
|
impl RuntimeDomSinksParserStep<'_> {
|
|
fn feed(&self, chunk: &str) {
|
|
self.stream.inner.feed(chunk);
|
|
}
|
|
|
|
fn pump_parser_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
self.stream.pump_parser_step(chunk)
|
|
}
|
|
|
|
fn pump_parser_inserted_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
self.stream.inner.pump_parser_inserted_step(chunk)
|
|
}
|
|
|
|
fn pump_next_parser_step(&self, max_bytes: usize) -> ParserPumpOutcome {
|
|
self.stream.pump_next_parser_step(max_bytes)
|
|
}
|
|
}
|
|
|
|
fn split_parser_input_prefix(input: String, max_bytes: usize) -> (String, Option<String>) {
|
|
if max_bytes == 0 || input.len() <= max_bytes {
|
|
return (input, None);
|
|
}
|
|
|
|
let mut split = input.len();
|
|
for (index, character) in input.char_indices() {
|
|
let end = index + character.len_utf8();
|
|
if end > max_bytes {
|
|
split = if index == 0 { end } else { index };
|
|
break;
|
|
}
|
|
}
|
|
let remainder = input[split..].to_owned();
|
|
let prefix = input[..split].to_owned();
|
|
(prefix, (!remainder.is_empty()).then_some(remainder))
|
|
}
|
|
|
|
impl Drop for RuntimeDomSinksParserStep<'_> {
|
|
fn drop(&mut self) {
|
|
self.stream.inner.clear_runtime_dom_sinks_after_parse_step();
|
|
}
|
|
}
|
|
|
|
struct RuntimeDomSinksParserFinish {
|
|
stream: Option<DocumentStream>,
|
|
}
|
|
|
|
impl RuntimeDomSinksParserFinish {
|
|
fn finish(&mut self) -> ParserFinishDiscoverySignals {
|
|
if let Some(stream) = self.stream.take() {
|
|
stream.inner.finish_live_runtime_dom_sink_parser()
|
|
} else {
|
|
ParserFinishDiscoverySignals::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for RuntimeDomSinksParserFinish {
|
|
fn drop(&mut self) {
|
|
if let Some(stream) = &mut self.stream {
|
|
stream.inner.clear_runtime_dom_sinks_after_parse_step();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParserStreamDocumentSnapshot {
|
|
pub fn into_document(self) -> NativeDom {
|
|
self.0
|
|
}
|
|
|
|
pub fn nodes(&self) -> NativeDomNodes<'_> {
|
|
self.0.nodes()
|
|
}
|
|
|
|
pub fn final_url(&self) -> Option<&url::Url> {
|
|
self.0.final_url()
|
|
}
|
|
|
|
pub fn document_base_url(&self) -> Option<url::Url> {
|
|
self.0
|
|
.document()
|
|
.map(|document| document.base_url().clone())
|
|
}
|
|
|
|
pub fn parse_errors(&self) -> &[String] {
|
|
self.0.parse_errors()
|
|
}
|
|
|
|
pub fn document_node_id(&self) -> NativeNodeId {
|
|
self.0.document_node_id()
|
|
}
|
|
|
|
pub fn document_body_handle(&self) -> Option<NativeNodeId> {
|
|
self.0.document_body_handle()
|
|
}
|
|
|
|
pub fn node(&self, node_id: NativeNodeId) -> Option<&Node> {
|
|
self.0.node(node_id)
|
|
}
|
|
|
|
pub fn child_ids(&self, node_id: NativeNodeId) -> impl Iterator<Item = NativeNodeId> + '_ {
|
|
self.0.child_ids(node_id)
|
|
}
|
|
|
|
pub fn stylesheet_candidate_handles_before_in_tree_scope(
|
|
&self,
|
|
tree_scope: NativeNodeId,
|
|
stop_at: Option<NativeNodeId>,
|
|
) -> Vec<NativeNodeId> {
|
|
self.0
|
|
.stylesheet_candidate_handles_before_in_tree_scope(tree_scope, stop_at)
|
|
}
|
|
|
|
pub fn text_content(&self, node_id: NativeNodeId) -> Option<String> {
|
|
self.0.text_content(node_id)
|
|
}
|
|
|
|
pub fn elements_by_tag_name(
|
|
&self,
|
|
root: NativeNodeId,
|
|
tag_name: &str,
|
|
include_root: bool,
|
|
) -> Vec<NativeNodeId> {
|
|
self.0.elements_by_tag_name(root, tag_name, include_root)
|
|
}
|
|
|
|
pub fn script_handles(&self) -> Vec<NativeNodeId> {
|
|
self.0.script_handles()
|
|
}
|
|
|
|
pub fn document_order_script_handles(&self) -> Vec<NativeNodeId> {
|
|
self.0.document_order_script_handles()
|
|
}
|
|
|
|
pub fn script_src(&self, node_id: NativeNodeId) -> Option<&str> {
|
|
self.0.script_src(node_id)
|
|
}
|
|
|
|
pub fn script_text(&self, node_id: NativeNodeId) -> Option<String> {
|
|
self.0.script_text(node_id)
|
|
}
|
|
|
|
pub fn node_is_parser_created(&self, node_id: NativeNodeId) -> bool {
|
|
self.0
|
|
.node(node_id)
|
|
.is_some_and(|node| node.flags().parser_created())
|
|
}
|
|
|
|
pub fn mark_script_already_started(&mut self, node_id: NativeNodeId) -> bool {
|
|
self.0
|
|
.node_mut(node_id)
|
|
.and_then(|node| node.data_mut().as_element_mut())
|
|
.is_some_and(|element| element.set_script_already_started(true))
|
|
}
|
|
}
|
|
|
|
impl From<NativeDom> for ParserStreamDocumentSnapshot {
|
|
fn from(document: NativeDom) -> Self {
|
|
Self(document)
|
|
}
|
|
}
|
|
|
|
impl From<ParserStreamDocumentSnapshot> for NativeDom {
|
|
fn from(snapshot: ParserStreamDocumentSnapshot) -> Self {
|
|
snapshot.0
|
|
}
|
|
}
|
|
|
|
impl ParserInputQueue {
|
|
pub fn session(&self) -> ParserInputSession {
|
|
ParserInputSession(self.0.clone())
|
|
}
|
|
|
|
pub fn take_next_script_input(&self) -> Option<String> {
|
|
self.0
|
|
.borrow_mut()
|
|
.script_input_queue
|
|
.pop_front()
|
|
.filter(|html| !html.is_empty())
|
|
}
|
|
|
|
pub fn next_script_input_len(&self) -> Option<usize> {
|
|
self.0.borrow().script_input_queue.front().map(String::len)
|
|
}
|
|
|
|
pub fn snapshot_script_input(&self) -> String {
|
|
self.0
|
|
.borrow()
|
|
.script_input_queue
|
|
.iter()
|
|
.fold(String::new(), |mut input, segment| {
|
|
input.push_str(segment);
|
|
input
|
|
})
|
|
}
|
|
|
|
pub fn has_script_input(&self) -> bool {
|
|
!self.0.borrow().script_input_queue.is_empty()
|
|
}
|
|
|
|
pub fn take_next_insertion_preload_input(&self) -> Option<String> {
|
|
self.0
|
|
.borrow_mut()
|
|
.insertion_preload_queue
|
|
.pop_front()
|
|
.filter(|html| !html.is_empty())
|
|
}
|
|
|
|
pub fn take_processed_insertion_meta_csp_count(&self) -> usize {
|
|
std::mem::take(&mut self.0.borrow_mut().processed_insertion_meta_csp_count)
|
|
}
|
|
}
|
|
|
|
impl ParserInputSession {
|
|
pub fn enqueue_script_input_html(&self, html: String) {
|
|
if html.is_empty() {
|
|
return;
|
|
}
|
|
let mut state = self.0.borrow_mut();
|
|
if let Some(tail) = state.script_input_queue.back_mut() {
|
|
tail.push_str(&html);
|
|
} else {
|
|
state.script_input_queue.push_back(html);
|
|
}
|
|
}
|
|
|
|
pub fn take_next_script_input_html(&self) -> Option<String> {
|
|
self.0
|
|
.borrow_mut()
|
|
.script_input_queue
|
|
.pop_front()
|
|
.filter(|html| !html.is_empty())
|
|
}
|
|
|
|
pub fn enter_pending_context(&self) -> ParserInputContext {
|
|
self.0.borrow_mut().pending_stack.push(String::new());
|
|
ParserInputContext {
|
|
session: self.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn enqueue_script_input_preload_html(&self, html: String) {
|
|
if html.is_empty() {
|
|
return;
|
|
}
|
|
let mut state = self.0.borrow_mut();
|
|
if let Some(tail) = state.insertion_preload_queue.back_mut() {
|
|
tail.push_str(&html);
|
|
} else {
|
|
state.insertion_preload_queue.push_back(html);
|
|
}
|
|
}
|
|
|
|
pub fn note_processed_insertion_meta_csp(&self, count: usize) {
|
|
let mut state = self.0.borrow_mut();
|
|
state.processed_insertion_meta_csp_count = state
|
|
.processed_insertion_meta_csp_count
|
|
.saturating_add(count);
|
|
}
|
|
|
|
pub fn take_current_script_input_html(&self) -> String {
|
|
self.0
|
|
.borrow_mut()
|
|
.pending_stack
|
|
.last_mut()
|
|
.map(std::mem::take)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn set_current_script_input_html(&self, html: String) {
|
|
let mut state = self.0.borrow_mut();
|
|
let Some(current) = state.pending_stack.last_mut() else {
|
|
return;
|
|
};
|
|
*current = html;
|
|
}
|
|
|
|
fn flush_and_pop_pending_context(&self) {
|
|
let mut state = self.0.borrow_mut();
|
|
let pending = state.pending_stack.pop().unwrap_or_default();
|
|
if pending.is_empty() {
|
|
return;
|
|
}
|
|
if let Some(tail) = state.script_input_queue.back_mut() {
|
|
tail.push_str(&pending);
|
|
} else {
|
|
state.script_input_queue.push_back(pending);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParserInputContext {
|
|
pub fn session(&self) -> ParserInputSession {
|
|
self.session.clone()
|
|
}
|
|
}
|
|
|
|
impl Drop for ParserInputContext {
|
|
fn drop(&mut self) {
|
|
self.session.flush_and_pop_pending_context();
|
|
}
|
|
}
|
|
|
|
impl ParseHandle {
|
|
pub(super) fn new(node_id: NativeNodeId, element_name: Option<Rc<QualName>>) -> Self {
|
|
Self {
|
|
identity: ParseHandleIdentity::DomNode(node_id),
|
|
element_name,
|
|
parser_flags: ParserElementFlags::default(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn new_synthetic_fragment_context(element_name: Rc<QualName>) -> Self {
|
|
Self {
|
|
identity: ParseHandleIdentity::SyntheticFragmentContext,
|
|
element_name: Some(element_name),
|
|
parser_flags: ParserElementFlags::default(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn new_element(
|
|
node_id: NativeNodeId,
|
|
element_name: Rc<QualName>,
|
|
parser_flags: ParserElementFlags,
|
|
) -> Self {
|
|
Self {
|
|
identity: ParseHandleIdentity::DomNode(node_id),
|
|
element_name: Some(element_name),
|
|
parser_flags,
|
|
}
|
|
}
|
|
|
|
pub(super) fn dom_node_id(&self) -> Option<NativeNodeId> {
|
|
match self.identity {
|
|
ParseHandleIdentity::DomNode(node_id) => Some(node_id),
|
|
ParseHandleIdentity::SyntheticFragmentContext => None,
|
|
}
|
|
}
|
|
|
|
fn is_script_element(&self) -> bool {
|
|
self.element_name.as_ref().is_some_and(|name| {
|
|
name.local.as_ref() == "script"
|
|
&& matches!(
|
|
name.ns.as_ref(),
|
|
"http://www.w3.org/1999/xhtml" | "http://www.w3.org/2000/svg"
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(super) fn node_id(&self) -> NativeNodeId {
|
|
self.dom_node_id()
|
|
.expect("parser operation requires a real DOM node handle")
|
|
}
|
|
}
|
|
|
|
impl PartialEq for ParseHandle {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.identity == other.identity
|
|
}
|
|
}
|
|
|
|
impl Eq for ParseHandle {}
|
|
|
|
impl DocumentSink {
|
|
pub(super) fn mark_source_positions_unknown(&self) {
|
|
self.source_positions_known.set(false);
|
|
let unknown = ParserSourcePosition::UNKNOWN;
|
|
self.target
|
|
.borrow_mut()
|
|
.set_current_position(unknown.line, unknown.column);
|
|
}
|
|
|
|
fn mutate_target(
|
|
&self,
|
|
mutation: impl FnOnce(&mut ParserStreamHtmlTreeSinkTarget) -> ParserMutationEffectDelivery,
|
|
) {
|
|
let delivery = {
|
|
let mut target = self.target.borrow_mut();
|
|
mutation(&mut target)
|
|
};
|
|
delivery.consume();
|
|
}
|
|
|
|
pub(super) fn finish_parser_dom_mutations(&self) -> std::ops::ControlFlow<()> {
|
|
// Do not retain the target borrow while invoking JavaScript. The tree
|
|
// builder calls this after updating its open-element stack, so nested
|
|
// document.write() sees the completed token's insertion state.
|
|
let finish = self.target.borrow().mutation_finisher();
|
|
finish.map_or(std::ops::ControlFlow::Continue(()), |finish| finish())
|
|
}
|
|
|
|
pub(super) fn new(target: ParserStreamHtmlTreeSinkTarget) -> Self {
|
|
Self {
|
|
target: RefCell::new(target),
|
|
source_positions_known: Cell::new(true),
|
|
html4_empty_system_id_quirks_override_pending: Cell::new(false),
|
|
}
|
|
}
|
|
|
|
pub(super) fn snapshot_parser_stream_document(&self) -> NativeDom {
|
|
self.target.borrow().snapshot_parser_stream_document()
|
|
}
|
|
|
|
pub(super) fn snapshot_parser_stream_dom_host(&self) -> DomHost {
|
|
self.target.borrow().snapshot_parser_stream_dom_host()
|
|
}
|
|
|
|
pub(super) fn take_parser_stream_null_custom_element_registry_elements(
|
|
&self,
|
|
) -> Vec<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.take_parser_stream_null_custom_element_registry_elements()
|
|
}
|
|
|
|
pub(super) fn take_parser_stream_dom_host(&self) -> DomHost {
|
|
self.target.borrow_mut().take_parser_stream_document()
|
|
}
|
|
|
|
pub(super) fn restore_parser_stream_dom_host(&self, dom_host: DomHost) {
|
|
self.target
|
|
.borrow_mut()
|
|
.restore_parser_stream_dom_host(dom_host);
|
|
}
|
|
|
|
pub(super) fn enter_runtime_dom_sinks_parse_step(&self, sinks: ParserRuntimeDomSinks) {
|
|
self.target
|
|
.borrow_mut()
|
|
.enter_runtime_dom_sinks_parse_step(sinks);
|
|
}
|
|
|
|
pub(super) fn clear_runtime_dom_sinks_after_parse_step(&self) {
|
|
self.target
|
|
.borrow_mut()
|
|
.clear_runtime_dom_sinks_after_parse_step()
|
|
}
|
|
|
|
pub(super) fn borrow_target(&self) -> Ref<'_, ParserStreamHtmlTreeSinkTarget> {
|
|
self.target.borrow()
|
|
}
|
|
|
|
pub(super) fn replace_parser_stream_document(&self, document: NativeDom) {
|
|
self.target
|
|
.borrow_mut()
|
|
.replace_parser_stream_document(document);
|
|
}
|
|
|
|
pub(super) fn drain_ready_parser_scripts(&self) -> Vec<NativeNodeId> {
|
|
self.target.borrow_mut().drain_ready_parser_scripts()
|
|
}
|
|
|
|
pub(super) fn drain_discovered_async_prefetch_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.drain_discovered_async_prefetch_candidates()
|
|
}
|
|
|
|
pub(super) fn drain_discovered_modulepreload_link_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.drain_discovered_modulepreload_link_candidates()
|
|
}
|
|
|
|
pub(super) fn drain_discovered_parser_meta_csp_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.drain_discovered_parser_meta_csp_candidates()
|
|
}
|
|
|
|
pub(super) fn note_defined_autonomous_custom_element(&self, local_name: &str) {
|
|
self.target
|
|
.borrow_mut()
|
|
.note_defined_autonomous_custom_element(local_name);
|
|
}
|
|
|
|
pub(super) fn drain_pending_custom_element_construction_handoffs(
|
|
&self,
|
|
) -> Vec<ParserCustomElementConstructionHandoff> {
|
|
self.target
|
|
.borrow_mut()
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
}
|
|
|
|
pub(super) fn pending_custom_element_construction_handoff_placeholder(
|
|
&self,
|
|
) -> Option<NativeNodeId> {
|
|
self.target
|
|
.borrow()
|
|
.front_pending_custom_element_construction_handoff()
|
|
.map(|handoff| handoff.placeholder)
|
|
}
|
|
|
|
pub(super) fn pop_pending_custom_element_construction_handoff(
|
|
&self,
|
|
) -> Option<ParserCustomElementConstructionHandoff> {
|
|
self.target
|
|
.borrow_mut()
|
|
.pop_pending_custom_element_construction_handoff()
|
|
}
|
|
|
|
pub(super) fn pending_blocking_stylesheet_pause(&self) -> Option<NativeNodeId> {
|
|
self.target
|
|
.borrow()
|
|
.front_pending_blocking_stylesheet_pause()
|
|
}
|
|
|
|
pub(super) fn pop_pending_blocking_stylesheet_pause(&self) -> Option<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.pop_pending_blocking_stylesheet_pause()
|
|
}
|
|
|
|
pub(super) fn begin_tree_builder_finish(&self) {
|
|
self.target.borrow_mut().begin_tree_builder_finish();
|
|
}
|
|
|
|
pub(super) fn drain_discovered_blocking_stylesheet_inputs(
|
|
&self,
|
|
) -> Vec<DocumentOwnedBlockingStylesheetDiscoveryInput> {
|
|
self.target
|
|
.borrow_mut()
|
|
.drain_discovered_blocking_stylesheet_inputs()
|
|
}
|
|
|
|
pub(super) fn captured_blocking_stylesheet_signatures(
|
|
&self,
|
|
) -> HashSet<DocumentBlockingStylesheetSignature> {
|
|
self.target
|
|
.borrow()
|
|
.captured_blocking_stylesheet_signatures()
|
|
}
|
|
|
|
pub(super) fn note_foreign_end_tag_processed(&self, local_name: &str) -> Option<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.note_foreign_end_tag_processed(local_name)
|
|
}
|
|
|
|
pub(super) fn note_self_closing_foreign_element_processed(
|
|
&self,
|
|
local_name: &str,
|
|
) -> Option<NativeNodeId> {
|
|
self.target
|
|
.borrow_mut()
|
|
.note_self_closing_foreign_element_processed(local_name)
|
|
}
|
|
|
|
pub(super) fn mark_script_already_started(&self, node_id: NativeNodeId) {
|
|
self.target
|
|
.borrow_mut()
|
|
.mark_script_already_started(node_id);
|
|
}
|
|
}
|
|
|
|
impl TreeSink for DocumentSink {
|
|
type Handle = ParseHandle;
|
|
type Output = ParserStreamHtmlTreeSinkTarget;
|
|
type ElemName<'a>
|
|
= &'a QualName
|
|
where
|
|
Self: 'a;
|
|
|
|
fn finish(self) -> Self::Output {
|
|
self.target.into_inner()
|
|
}
|
|
|
|
fn parse_error(&self, err: Cow<'static, str>) {
|
|
self.target.borrow_mut().push_parse_error(err.into_owned());
|
|
}
|
|
|
|
fn get_document(&self) -> Self::Handle {
|
|
self.target.borrow().document_handle()
|
|
}
|
|
|
|
fn set_quirks_mode(&self, mode: QuirksMode) {
|
|
// html5ever 0.39 distinguishes `Some("")` from a missing system ID
|
|
// when selecting HTML 4.01 limited-quirks mode. The HTML Standard now
|
|
// puts both in quirks mode. Keep the token and its parse-error state
|
|
// intact, and only correct the mode reported immediately after the
|
|
// initial doctype callback.
|
|
let mode = if self
|
|
.html4_empty_system_id_quirks_override_pending
|
|
.replace(false)
|
|
&& mode == QuirksMode::LimitedQuirks
|
|
{
|
|
QuirksMode::Quirks
|
|
} else {
|
|
mode
|
|
};
|
|
self.target.borrow_mut().set_html_quirks_mode(mode);
|
|
}
|
|
|
|
fn same_node(&self, left: &Self::Handle, right: &Self::Handle) -> bool {
|
|
left == right
|
|
}
|
|
|
|
fn elem_name<'a>(&'a self, target: &'a Self::Handle) -> Self::ElemName<'a> {
|
|
target
|
|
.element_name
|
|
.as_deref()
|
|
.expect("html5ever requested the name of a non-element node")
|
|
}
|
|
|
|
fn is_mathml_annotation_xml_integration_point(&self, target: &Self::Handle) -> bool {
|
|
target.parser_flags.mathml_annotation_xml_integration_point
|
|
}
|
|
|
|
fn create_element(
|
|
&self,
|
|
name: QualName,
|
|
attrs: Vec<Attribute>,
|
|
flags: ElementFlags,
|
|
) -> Self::Handle {
|
|
self.target.borrow_mut().create_element(name, attrs, flags)
|
|
}
|
|
|
|
fn create_comment(&self, text: StrTendril) -> Self::Handle {
|
|
self.target.borrow_mut().create_comment(text.to_string())
|
|
}
|
|
|
|
fn create_pi(&self, target: StrTendril, data: StrTendril) -> Self::Handle {
|
|
self.target
|
|
.borrow_mut()
|
|
.create_processing_instruction(target.to_string(), data.to_string())
|
|
}
|
|
|
|
fn append(&self, parent: &Self::Handle, child: NodeOrText<Self::Handle>) {
|
|
self.mutate_target(|target| target.append(parent.node_id(), child));
|
|
}
|
|
|
|
fn append_before_sibling(&self, sibling: &Self::Handle, child: NodeOrText<Self::Handle>) {
|
|
self.mutate_target(|target| target.append_before_sibling(sibling.node_id(), child));
|
|
}
|
|
|
|
fn append_based_on_parent_node(
|
|
&self,
|
|
element: &Self::Handle,
|
|
prev_element: &Self::Handle,
|
|
child: NodeOrText<Self::Handle>,
|
|
) {
|
|
self.mutate_target(|target| {
|
|
target.append_based_on_parent_node(element.node_id(), prev_element.node_id(), child)
|
|
});
|
|
}
|
|
|
|
fn append_doctype_to_document(
|
|
&self,
|
|
name: StrTendril,
|
|
public_id: StrTendril,
|
|
system_id: StrTendril,
|
|
) {
|
|
// TreeSink receives an empty tendril for both a missing and an explicit
|
|
// empty system identifier. Those cases intentionally share the same
|
|
// quirks-mode outcome for these two HTML 4.01 public identifiers.
|
|
self.html4_empty_system_id_quirks_override_pending.set(
|
|
html4_doctype_without_nonempty_system_id_requires_quirks(
|
|
name.as_ref(),
|
|
public_id.as_ref(),
|
|
system_id.as_ref(),
|
|
),
|
|
);
|
|
self.mutate_target(|target| {
|
|
target.append_doctype(
|
|
name.to_string(),
|
|
public_id.to_string(),
|
|
system_id.to_string(),
|
|
)
|
|
});
|
|
}
|
|
|
|
fn get_template_contents(&self, target: &Self::Handle) -> Self::Handle {
|
|
target
|
|
.dom_node_id()
|
|
.and_then(|node_id| self.target.borrow_mut().template_contents_handle(node_id))
|
|
.unwrap_or_else(|| target.clone())
|
|
}
|
|
|
|
fn attach_declarative_shadow(
|
|
&self,
|
|
location: &Self::Handle,
|
|
template: &Self::Handle,
|
|
attrs: &[Attribute],
|
|
) -> bool {
|
|
let Some(location_id) = location.dom_node_id() else {
|
|
return false;
|
|
};
|
|
let Some(template_id) = template.dom_node_id() else {
|
|
return false;
|
|
};
|
|
self.target
|
|
.borrow_mut()
|
|
.attach_declarative_shadow(location_id, template_id, attrs)
|
|
}
|
|
|
|
fn add_attrs_if_missing(&self, target: &Self::Handle, attrs: Vec<Attribute>) {
|
|
if let Some(target_id) = target.dom_node_id() {
|
|
self.target
|
|
.borrow_mut()
|
|
.add_attrs_if_missing(target_id, attrs);
|
|
}
|
|
}
|
|
|
|
fn associate_with_form(
|
|
&self,
|
|
target: &Self::Handle,
|
|
form: &Self::Handle,
|
|
_nodes: (&Self::Handle, Option<&Self::Handle>),
|
|
) {
|
|
let Some(target_id) = target.dom_node_id() else {
|
|
return;
|
|
};
|
|
let Some(form_id) = form.dom_node_id() else {
|
|
return;
|
|
};
|
|
self.target
|
|
.borrow_mut()
|
|
.associate_with_form(target_id, form_id);
|
|
}
|
|
|
|
fn remove_from_parent(&self, target: &Self::Handle) {
|
|
if let Some(target_id) = target.dom_node_id() {
|
|
self.mutate_target(|sink| sink.remove_from_parent(target_id));
|
|
}
|
|
}
|
|
|
|
fn reparent_children(&self, node: &Self::Handle, new_parent: &Self::Handle) {
|
|
let Some(node_id) = node.dom_node_id() else {
|
|
return;
|
|
};
|
|
let Some(new_parent_id) = new_parent.dom_node_id() else {
|
|
return;
|
|
};
|
|
self.mutate_target(|target| target.reparent_children(node_id, new_parent_id));
|
|
}
|
|
|
|
fn mark_script_already_started(&self, node: &Self::Handle) {
|
|
// html5ever may mark a script "already started" as part of its tree-builder state
|
|
// machine. Keep that flag in the backing DOM as well so any snapshot handed to later
|
|
// runtime/planning code preserves the same script bookkeeping contract.
|
|
if let Some(node_id) = node.dom_node_id() {
|
|
self.target
|
|
.borrow_mut()
|
|
.mark_script_already_started(node_id);
|
|
}
|
|
}
|
|
|
|
fn pop(&self, node: &Self::Handle) {
|
|
// html5ever calls `pop()` when the element has been fully closed by the parser. For
|
|
// classic parser-inserted scripts that is the earliest safe moment to hand execution back
|
|
// to the runtime without risking partial inline source or a half-built element subtree.
|
|
if let Some(node_id) = node.dom_node_id() {
|
|
self.target
|
|
.borrow_mut()
|
|
.note_node_closed(node_id, node.is_script_element());
|
|
}
|
|
}
|
|
|
|
fn set_current_line(&self, line_number: u64) {
|
|
let mut target = self.target.borrow_mut();
|
|
if self.source_positions_known.get() {
|
|
let position = ParserSourcePosition::line_only(line_number);
|
|
target.set_current_position(position.line, position.column);
|
|
} else {
|
|
let unknown = ParserSourcePosition::UNKNOWN;
|
|
target.set_current_position(unknown.line, unknown.column);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::rc::Rc;
|
|
|
|
use super::{HtmlParser, ParseHandle, ParserInputQueue};
|
|
use html5ever::{LocalName, Namespace, QualName};
|
|
use moli_dom::native::{NativeDom, NativeNodeId};
|
|
use url::Url;
|
|
|
|
const HTML_NS: &str = "http://www.w3.org/1999/xhtml";
|
|
const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML";
|
|
|
|
fn parse_test_document(html: &str) -> NativeDom {
|
|
HtmlParser::SCRIPTING_ENABLED.parse(
|
|
Url::parse("https://example.test/").expect("test url"),
|
|
html.to_owned(),
|
|
)
|
|
}
|
|
|
|
fn parse_test_document_with_scripting(html: &str, scripting_enabled: bool) -> NativeDom {
|
|
let stream = HtmlParser::with_scripting_enabled(scripting_enabled)
|
|
.start_document(Url::parse("https://example.test/").expect("test url"));
|
|
stream.feed(html);
|
|
stream.finish()
|
|
}
|
|
|
|
fn first_element_by_ns(
|
|
document: &NativeDom,
|
|
namespace: &str,
|
|
local_name: &str,
|
|
) -> NativeNodeId {
|
|
document
|
|
.elements_by_tag_name_ns(
|
|
document.document_node_id(),
|
|
Some(namespace),
|
|
local_name,
|
|
true,
|
|
)
|
|
.into_iter()
|
|
.next()
|
|
.unwrap_or_else(|| panic!("expected {namespace} {local_name} element"))
|
|
}
|
|
|
|
#[test]
|
|
fn standalone_fragment_context_handle_is_not_the_document_handle() {
|
|
let document = ParseHandle::new(NativeNodeId::new(0), None);
|
|
let context = Rc::new(QualName::new(
|
|
None,
|
|
Namespace::from(HTML_NS),
|
|
LocalName::from("body"),
|
|
));
|
|
let fragment_context = ParseHandle::new_synthetic_fragment_context(context);
|
|
|
|
assert_ne!(fragment_context, document);
|
|
assert_eq!(fragment_context.dom_node_id(), None);
|
|
assert_eq!(document.dom_node_id(), Some(NativeNodeId::new(0)));
|
|
assert_eq!(
|
|
fragment_context
|
|
.element_name
|
|
.as_deref()
|
|
.map(|name| name.local.as_ref()),
|
|
Some("body")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_scripting_flag_controls_noscript_tokenization() {
|
|
let html = concat!(
|
|
"<!doctype html><html><head>",
|
|
"<noscript><link id='fallback-style' rel='stylesheet' href='/fallback.css'></noscript>",
|
|
"</head><body>",
|
|
"<noscript><main id='fallback'>fallback</main></noscript>",
|
|
"</body></html>",
|
|
);
|
|
|
|
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("<html></html>");
|
|
let no_quirks = mode("<!doctype html><html></html>");
|
|
let limited_quirks = mode(concat!(
|
|
"<!doctype html PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//\" ",
|
|
"\"https://www.w3.org/TR/html4/frameset.dtd\"><html></html>",
|
|
));
|
|
|
|
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!("<!doctype html PUBLIC \"{public_id}\"><html></html>");
|
|
let empty = format!("<!doctype html PUBLIC \"{public_id}\" \"\"><html></html>");
|
|
let nonempty =
|
|
format!("<!doctype html PUBLIC \"{public_id}\" \"legacy.dtd\"><html></html>");
|
|
|
|
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!(
|
|
"<!doctype html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//\" ",
|
|
"\"\"><html></html>",
|
|
)),
|
|
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",
|
|
"<noscript><span id='fallback'></span></noscript>".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!(
|
|
"<!doctype html>",
|
|
"<template>",
|
|
"<link rel='stylesheet' href='/inside.css'>",
|
|
"<style>@import url('/inside-import.css');</style>",
|
|
"</template>",
|
|
)
|
|
.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!(
|
|
"<!doctype html>",
|
|
"<math><annotation-xml encoding='text/html'><div></div></annotation-xml></math>"
|
|
));
|
|
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!(
|
|
"<!doctype html>",
|
|
"<div><math><annotation-xml><p></p></annotation-xml></math></div>"
|
|
));
|
|
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!(
|
|
"<!doctype html>",
|
|
"<select>",
|
|
"<option>one</option>",
|
|
"<div id='wrapper'><option>two</option>",
|
|
"<div id='nested'><option>three</option></div></div>",
|
|
"</select>"
|
|
));
|
|
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",
|
|
"<div id='fragment-wrapper'><option>value</option></div>".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 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("<scr".to_owned());
|
|
|
|
let inner = session.enter_pending_context();
|
|
session.set_current_script_input_html("<div>inner".to_owned());
|
|
assert_eq!(session.take_current_script_input_html(), "<div>inner");
|
|
session.set_current_script_input_html("<div>inner".to_owned());
|
|
drop(inner);
|
|
|
|
assert_eq!(
|
|
queue.take_next_script_input().as_deref(),
|
|
Some("<div>inner")
|
|
);
|
|
assert_eq!(outer.session().take_current_script_input_html(), "<scr");
|
|
outer
|
|
.session()
|
|
.set_current_script_input_html("<script>outer</script>".to_owned());
|
|
drop(outer);
|
|
|
|
assert_eq!(
|
|
queue.take_next_script_input().as_deref(),
|
|
Some("<script>outer</script>")
|
|
);
|
|
}
|
|
|
|
#[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("<script>script-input</script>".to_owned());
|
|
session.enqueue_script_input_preload_html("<scr".to_owned());
|
|
session.enqueue_script_input_preload_html("ipt src=\"/write.js\"></script>".to_owned());
|
|
|
|
assert_eq!(
|
|
queue.take_next_insertion_preload_input().as_deref(),
|
|
Some("<script src=\"/write.js\"></script>")
|
|
);
|
|
assert_eq!(
|
|
queue.take_next_script_input().as_deref(),
|
|
Some("<script>script-input</script>")
|
|
);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|