mirror of
https://github.com/lexmount/moli.git
synced 2026-10-02 16:00:44 +00:00
Reuse the claimed stylesheet pause in deferred preparation and use the shared source suspension cause for popup streams. Exercise document.write resume ownership with a body stylesheet boundary so nested inline execution cannot bypass the intended suspension.
2828 lines
110 KiB
Rust
2828 lines
110 KiB
Rust
use html5ever::tendril::StrTendril;
|
|
use moli_dom::{
|
|
NodeId,
|
|
native::{DomHost, NativeDom, NativeNodeId},
|
|
};
|
|
use moli_page_types::{ScriptKind, ScriptMode, ScriptRun, ScriptSkipReason, ScriptSourceKind};
|
|
use moli_script::ScriptPreparationDisposition;
|
|
use moli_stylesheet_blocking::{
|
|
DocumentBlockingStylesheetSignature, StylesheetBlockingReadView,
|
|
collect_document_owned_blocking_stylesheets_before_in_view,
|
|
};
|
|
use std::{
|
|
cell::{Cell, RefCell},
|
|
collections::HashMap,
|
|
};
|
|
|
|
use crate::script_planning::{
|
|
ParserPlanningReadView, PrepareScriptOutcome, PreparedImportMap, PreparedScript,
|
|
ScriptClassification, ScriptFilterSkipReason, build_prepared_import_map, build_prepared_script,
|
|
classify_parser_script,
|
|
};
|
|
|
|
use super::{
|
|
html::{
|
|
ParserBlockingStylesheetPause, ParserFinishDiscoverySignals, ParserInputContext,
|
|
ParserInputQueue, ParserInputSession, ParserPumpOutcome, ParserPumpStep,
|
|
ParserScriptElementStateTransition, ParserScriptHandoff, ParserScriptNoExecutionOutcome,
|
|
ParserScriptPreparationFailure, ParserScriptPreparationRequest, ParserYield,
|
|
},
|
|
live_target::{ParserRuntimeDomSinks, ParserStreamHtmlTreeSinkTarget},
|
|
session::{
|
|
HtmlParserSession, HtmlParserSessionResult, HtmlTreeSinkSession,
|
|
new_fragment_html_tree_sink_session, new_html_tree_sink_session,
|
|
},
|
|
};
|
|
|
|
pub(super) struct HtmlTreeSinkStream {
|
|
parser: HtmlParserSession,
|
|
script_input: ParserInputQueue,
|
|
parser_script_positions: RefCell<HashMap<NativeNodeId, usize>>,
|
|
next_parser_script_position: Cell<usize>,
|
|
defer_script_preparation: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum RawParserStep {
|
|
OwnerInterrupted,
|
|
Script(NativeNodeId),
|
|
CustomElementConstruction,
|
|
BlockingStylesheet(NativeNodeId),
|
|
InputDrained,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum ParserScriptPreparation {
|
|
BlockingClassic(PreparedScript),
|
|
AsyncPostParse(PreparedScript),
|
|
NonAsyncPostParse(PreparedScript),
|
|
ImportMap(PreparedImportMap),
|
|
NoExecution(ParserScriptNoExecutionOutcome),
|
|
PreparationFailure(ParserScriptPreparationFailure),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum ParserScriptPreparationLane {
|
|
BlockingClassic,
|
|
AsyncPostParse,
|
|
NonAsyncPostParse,
|
|
}
|
|
|
|
impl ParserScriptPreparation {
|
|
fn async_prefetch_script(&self) -> Option<PreparedScript> {
|
|
match self {
|
|
Self::AsyncPostParse(script) => Some(script.clone()),
|
|
Self::BlockingClassic(_)
|
|
| Self::NonAsyncPostParse(_)
|
|
| Self::ImportMap(_)
|
|
| Self::NoExecution(_)
|
|
| Self::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
fn is_blocking_classic(&self) -> bool {
|
|
matches!(self, Self::BlockingClassic(_))
|
|
}
|
|
|
|
fn into_handoff(
|
|
self,
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
blocking_signatures_before: std::collections::HashSet<DocumentBlockingStylesheetSignature>,
|
|
) -> ParserScriptHandoff {
|
|
match self {
|
|
Self::BlockingClassic(script) => ParserScriptHandoff::BlockingClassic {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
blocking_signatures_before,
|
|
script,
|
|
},
|
|
Self::AsyncPostParse(script) => ParserScriptHandoff::AsyncPostParse {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
script,
|
|
},
|
|
Self::NonAsyncPostParse(script) => ParserScriptHandoff::NonAsyncPostParse {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
blocking_signatures_before,
|
|
script,
|
|
},
|
|
Self::ImportMap(import_map) => ParserScriptHandoff::ImportMap {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
import_map,
|
|
},
|
|
Self::NoExecution(outcome) => ParserScriptHandoff::NoExecution {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
outcome,
|
|
},
|
|
Self::PreparationFailure(failure) => ParserScriptHandoff::PreparationFailure {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
failure,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unprepared_script_mode(
|
|
classification: &crate::script_planning::ScriptClassification,
|
|
) -> ScriptMode {
|
|
classification
|
|
.mode()
|
|
.unwrap_or_else(|| match classification.kind() {
|
|
ScriptKind::ImportMap => ScriptMode::ImportMapInOrder,
|
|
ScriptKind::DataBlock => ScriptMode::Normal,
|
|
ScriptKind::Classic | ScriptKind::Module => {
|
|
unreachable!("executable script kinds always have a mode")
|
|
}
|
|
})
|
|
}
|
|
|
|
fn ignored_parser_script(
|
|
position: usize,
|
|
mode: ScriptMode,
|
|
transition: ParserScriptElementStateTransition,
|
|
) -> ParserScriptPreparation {
|
|
ParserScriptPreparation::NoExecution(
|
|
ParserScriptNoExecutionOutcome::ignored(position, mode)
|
|
.with_element_state_transition(transition),
|
|
)
|
|
}
|
|
|
|
fn failed_parser_script(
|
|
position: usize,
|
|
mode: ScriptMode,
|
|
message: String,
|
|
transition: ParserScriptElementStateTransition,
|
|
) -> ParserScriptPreparation {
|
|
ParserScriptPreparation::PreparationFailure(
|
|
ParserScriptPreparationFailure::new(position, mode, message)
|
|
.with_element_state_transition(transition),
|
|
)
|
|
}
|
|
|
|
fn parser_script_report_url(
|
|
document: &impl ParserPlanningReadView,
|
|
classification: &ScriptClassification,
|
|
) -> Option<url::Url> {
|
|
let document_url = document.final_url_clone()?;
|
|
let Some(src) = classification.script.script_src.as_deref() else {
|
|
return Some(document_url);
|
|
};
|
|
Some(
|
|
document
|
|
.document_base_url_clone()
|
|
.unwrap_or_else(|| document_url.clone())
|
|
.join(src)
|
|
.unwrap_or(document_url),
|
|
)
|
|
}
|
|
|
|
fn skipped_parser_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
position: usize,
|
|
mode: ScriptMode,
|
|
classification: &ScriptClassification,
|
|
reason: ScriptSkipReason,
|
|
transition: ParserScriptElementStateTransition,
|
|
) -> ParserScriptPreparation {
|
|
let Some(url) = parser_script_report_url(document, classification) else {
|
|
return ignored_parser_script(position, mode, transition);
|
|
};
|
|
ParserScriptPreparation::NoExecution(
|
|
ParserScriptNoExecutionOutcome::skipped(
|
|
position,
|
|
mode,
|
|
ScriptRun::skipped(
|
|
NodeId::new(node_id.index()),
|
|
classification.kind(),
|
|
mode,
|
|
classification.source_kind,
|
|
url,
|
|
reason,
|
|
),
|
|
)
|
|
.with_element_state_transition(transition),
|
|
)
|
|
}
|
|
|
|
fn consume_parser_inserted_transition(
|
|
classification: &ScriptClassification,
|
|
) -> ParserScriptElementStateTransition {
|
|
if classification.script.parser_inserted {
|
|
ParserScriptElementStateTransition::ConsumeParserInserted {
|
|
force_async: !classification.script.async_attribute_present,
|
|
}
|
|
} else {
|
|
ParserScriptElementStateTransition::None
|
|
}
|
|
}
|
|
|
|
fn parser_filter_skip_reason(
|
|
classification: &ScriptClassification,
|
|
reason: ScriptFilterSkipReason,
|
|
) -> ScriptSkipReason {
|
|
match reason {
|
|
ScriptFilterSkipReason::AlreadyStarted => ScriptSkipReason::AlreadyStarted,
|
|
ScriptFilterSkipReason::NoModule => ScriptSkipReason::NoModule,
|
|
ScriptFilterSkipReason::DataBlock => ScriptSkipReason::UnsupportedType(
|
|
classification
|
|
.script
|
|
.script_type
|
|
.clone()
|
|
.unwrap_or_default(),
|
|
),
|
|
ScriptFilterSkipReason::LegacyEventForMismatch => ScriptSkipReason::UnsupportedType(
|
|
"legacy for/event script did not match window.onload".to_owned(),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn prepare_parser_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
parser_position: Option<usize>,
|
|
) -> ParserScriptPreparation {
|
|
let connected = document.is_connected(node_id);
|
|
let position = parser_position.or_else(|| document.document_order_position(node_id));
|
|
let fallback_position = position.unwrap_or_else(|| document.script_handles().len());
|
|
let Some(c) = classify_parser_script(document, node_id) else {
|
|
return failed_parser_script(
|
|
fallback_position,
|
|
ScriptMode::Normal,
|
|
"script node disappeared during parser preparation".to_owned(),
|
|
ParserScriptElementStateTransition::None,
|
|
);
|
|
};
|
|
let classification_mode = unprepared_script_mode(&c);
|
|
if !connected {
|
|
return skipped_parser_script(
|
|
document,
|
|
node_id,
|
|
fallback_position,
|
|
classification_mode,
|
|
&c,
|
|
ScriptSkipReason::NotInMainDocument,
|
|
consume_parser_inserted_transition(&c),
|
|
);
|
|
}
|
|
if let Some(reason) = c.skip_reason() {
|
|
let transition = match reason {
|
|
ScriptFilterSkipReason::AlreadyStarted => ParserScriptElementStateTransition::None,
|
|
ScriptFilterSkipReason::DataBlock => consume_parser_inserted_transition(&c),
|
|
ScriptFilterSkipReason::NoModule | ScriptFilterSkipReason::LegacyEventForMismatch => {
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted
|
|
}
|
|
};
|
|
return skipped_parser_script(
|
|
document,
|
|
node_id,
|
|
fallback_position,
|
|
classification_mode,
|
|
&c,
|
|
parser_filter_skip_reason(&c, reason),
|
|
transition,
|
|
);
|
|
}
|
|
|
|
if c.disposition == ScriptPreparationDisposition::ImportMap {
|
|
let Some(position) = position else {
|
|
return failed_parser_script(
|
|
fallback_position,
|
|
classification_mode,
|
|
"script node is not in document order during parser preparation".to_owned(),
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
);
|
|
};
|
|
let Some(final_url) = document.final_url_clone() else {
|
|
return failed_parser_script(
|
|
position,
|
|
classification_mode,
|
|
"document URL missing during parser script preparation".to_owned(),
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
);
|
|
};
|
|
let document_base_url = document
|
|
.document_base_url_clone()
|
|
.unwrap_or_else(|| final_url.clone());
|
|
return build_prepared_import_map(&c, final_url, document_base_url, node_id, position)
|
|
.map(ParserScriptPreparation::ImportMap)
|
|
.unwrap_or_else(|| {
|
|
failed_parser_script(
|
|
position,
|
|
classification_mode,
|
|
"failed to prepare parser import map".to_owned(),
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
)
|
|
});
|
|
}
|
|
let Some((kind, mode)) = c.executable() else {
|
|
return ignored_parser_script(
|
|
fallback_position,
|
|
classification_mode,
|
|
consume_parser_inserted_transition(&c),
|
|
);
|
|
};
|
|
let lane = match (kind, mode, c.source_kind) {
|
|
(ScriptKind::Classic, ScriptMode::Normal, _) => {
|
|
ParserScriptPreparationLane::BlockingClassic
|
|
}
|
|
(ScriptKind::Classic, ScriptMode::Async, ScriptSourceKind::External)
|
|
| (ScriptKind::Module, ScriptMode::Async, _) => ParserScriptPreparationLane::AsyncPostParse,
|
|
(
|
|
ScriptKind::Classic,
|
|
ScriptMode::Defer | ScriptMode::ModuleDefer,
|
|
ScriptSourceKind::External,
|
|
)
|
|
| (ScriptKind::Module, _, _) => ParserScriptPreparationLane::NonAsyncPostParse,
|
|
_ => {
|
|
return ignored_parser_script(
|
|
fallback_position,
|
|
mode,
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
);
|
|
}
|
|
};
|
|
|
|
let Some(position) = position else {
|
|
return failed_parser_script(
|
|
fallback_position,
|
|
mode,
|
|
"script node is not in document order during parser preparation".to_owned(),
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
);
|
|
};
|
|
let Some(final_url) = document.final_url_clone() else {
|
|
return failed_parser_script(
|
|
position,
|
|
mode,
|
|
"document URL missing during parser script preparation".to_owned(),
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
);
|
|
};
|
|
let document_base_url = document
|
|
.document_base_url_clone()
|
|
.unwrap_or_else(|| final_url.clone());
|
|
match build_prepared_script(&c, final_url, document_base_url, node_id, position) {
|
|
PrepareScriptOutcome::Prepared(script) => match lane {
|
|
ParserScriptPreparationLane::BlockingClassic => {
|
|
ParserScriptPreparation::BlockingClassic(*script)
|
|
}
|
|
ParserScriptPreparationLane::AsyncPostParse => {
|
|
ParserScriptPreparation::AsyncPostParse(*script)
|
|
}
|
|
ParserScriptPreparationLane::NonAsyncPostParse => {
|
|
ParserScriptPreparation::NonAsyncPostParse(*script)
|
|
}
|
|
},
|
|
PrepareScriptOutcome::UrlResolutionFailed(error)
|
|
| PrepareScriptOutcome::EmptyExternalSource(error) => {
|
|
ParserScriptPreparation::PreparationFailure(
|
|
ParserScriptPreparationFailure::external_source(position, mode, error)
|
|
.with_element_state_transition(
|
|
ParserScriptElementStateTransition::MarkAlreadyStarted,
|
|
),
|
|
)
|
|
}
|
|
PrepareScriptOutcome::EmptyInlineSource => skipped_parser_script(
|
|
document,
|
|
node_id,
|
|
position,
|
|
mode,
|
|
&c,
|
|
ScriptSkipReason::EmptyInlineScript,
|
|
consume_parser_inserted_transition(&c),
|
|
),
|
|
PrepareScriptOutcome::NonExecutableKind(_) => {
|
|
ignored_parser_script(position, mode, consume_parser_inserted_transition(&c))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn prepare_parser_script_handoff_for_static_document(
|
|
document: &(impl ParserPlanningReadView + StylesheetBlockingReadView),
|
|
node_id: NativeNodeId,
|
|
start_line: u64,
|
|
start_column: u64,
|
|
) -> ParserScriptHandoff {
|
|
let blocking_signatures_before = collect_document_owned_blocking_stylesheets_before_in_view(
|
|
document,
|
|
NodeId::new(node_id.index()),
|
|
)
|
|
.into_iter()
|
|
.map(|blocker| blocker.signature().clone())
|
|
.collect();
|
|
prepare_parser_script(document, node_id, None).into_handoff(
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
blocking_signatures_before,
|
|
)
|
|
}
|
|
|
|
impl HtmlTreeSinkStream {
|
|
pub(super) fn initialize_text_document(&self) {
|
|
self.parser.initialize_text_document();
|
|
}
|
|
|
|
fn from_session(session: HtmlTreeSinkSession) -> Self {
|
|
Self {
|
|
parser: session.parser,
|
|
script_input: session.script_input,
|
|
parser_script_positions: RefCell::default(),
|
|
next_parser_script_position: Cell::new(0),
|
|
defer_script_preparation: false,
|
|
}
|
|
}
|
|
|
|
pub fn defer_script_preparation_to_owner(&mut self) {
|
|
self.defer_script_preparation = true;
|
|
}
|
|
|
|
pub fn prepare_script(&self, request: ParserScriptPreparationRequest) -> ParserScriptHandoff {
|
|
let target = self.parser.sink().borrow_target();
|
|
prepare_parser_script(&*target, request.node_id, Some(request.position)).into_handoff(
|
|
request.node_id,
|
|
request.start_line,
|
|
request.start_column,
|
|
request.blocking_signatures_before,
|
|
)
|
|
}
|
|
|
|
pub(super) fn from_target_with_scripting(
|
|
target: ParserStreamHtmlTreeSinkTarget,
|
|
scripting_enabled: bool,
|
|
) -> Self {
|
|
Self::from_target_with_options(
|
|
target,
|
|
super::session::html_parse_opts_with_scripting(scripting_enabled),
|
|
)
|
|
}
|
|
|
|
pub(super) fn from_target_with_options(
|
|
target: ParserStreamHtmlTreeSinkTarget,
|
|
options: html5ever::ParseOpts,
|
|
) -> Self {
|
|
Self::from_session(new_html_tree_sink_session(target, options))
|
|
}
|
|
|
|
pub(super) fn from_fragment_target(
|
|
target: ParserStreamHtmlTreeSinkTarget,
|
|
context_handle: NativeNodeId,
|
|
context_namespace: &str,
|
|
context_local_name: &str,
|
|
scripting_enabled: bool,
|
|
) -> Self {
|
|
let session = new_fragment_html_tree_sink_session(
|
|
target,
|
|
context_handle,
|
|
context_namespace,
|
|
context_local_name,
|
|
scripting_enabled,
|
|
);
|
|
Self::from_session(session)
|
|
}
|
|
|
|
fn parser_script_position(&self, node_id: NativeNodeId) -> usize {
|
|
*self
|
|
.parser_script_positions
|
|
.borrow_mut()
|
|
.entry(node_id)
|
|
.or_insert_with(|| {
|
|
let position = self.next_parser_script_position.get();
|
|
self.next_parser_script_position
|
|
.set(position.saturating_add(1));
|
|
position
|
|
})
|
|
}
|
|
|
|
pub fn script_input_session(&self) -> ParserInputSession {
|
|
self.script_input.session()
|
|
}
|
|
|
|
pub fn enter_script_input_context(&self) -> ParserInputContext {
|
|
self.script_input_session()
|
|
.enter_pending_context(self.parser.current_input_insertion_point())
|
|
}
|
|
|
|
pub fn take_next_script_input(&self) -> Option<String> {
|
|
self.script_input.take_next_script_input()
|
|
}
|
|
|
|
pub fn next_script_input_len(&self) -> Option<usize> {
|
|
self.script_input.next_script_input_len()
|
|
}
|
|
|
|
pub fn snapshot_script_input(&self) -> String {
|
|
self.script_input.snapshot_script_input()
|
|
}
|
|
|
|
pub fn has_script_input(&self) -> bool {
|
|
self.script_input.has_script_input()
|
|
}
|
|
|
|
pub fn take_next_insertion_preload_input(&self) -> Option<String> {
|
|
self.script_input.take_next_insertion_preload_input()
|
|
}
|
|
|
|
pub fn take_processed_insertion_meta_csp_count(&self) -> usize {
|
|
self.script_input.take_processed_insertion_meta_csp_count()
|
|
}
|
|
|
|
pub fn feed(&self, chunk: &str) {
|
|
if chunk.is_empty() {
|
|
return;
|
|
}
|
|
|
|
self.parser.process(StrTendril::from(chunk));
|
|
}
|
|
|
|
pub fn pump_parser_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
self.pump_parser_step_with_source(chunk, false)
|
|
}
|
|
|
|
pub fn pump_parser_inserted_step(&self, chunk: &str) -> ParserPumpOutcome {
|
|
if chunk.is_empty() {
|
|
return ParserPumpOutcome {
|
|
result: ParserPumpStep::InputDrained,
|
|
discovered_async_prefetch_scripts: Vec::new(),
|
|
discovered_preload_link_candidates: Vec::new(),
|
|
discovered_blocking_stylesheet_inputs: Vec::new(),
|
|
};
|
|
}
|
|
self.pump_parser_step_with_source(chunk, true)
|
|
}
|
|
|
|
pub fn append_to_current_inserted_input(&self, chunk: &str) -> bool {
|
|
self.parser
|
|
.append_to_current_inserted_input(StrTendril::from(chunk))
|
|
}
|
|
|
|
pub fn append_at_current_insertion_point(&self, chunk: &str) {
|
|
self.parser
|
|
.append_at_current_insertion_point(StrTendril::from(chunk));
|
|
}
|
|
|
|
fn pump_parser_step_with_source(
|
|
&self,
|
|
chunk: &str,
|
|
inserted_source: bool,
|
|
) -> ParserPumpOutcome {
|
|
if !chunk.is_empty() {
|
|
if inserted_source {
|
|
self.parser.begin_inserted_input(StrTendril::from(chunk));
|
|
} else {
|
|
self.parser.push_back(StrTendril::from(chunk));
|
|
}
|
|
}
|
|
|
|
let tokenizer_result = self.parser.feed();
|
|
let paused_for_custom_element = matches!(
|
|
&tokenizer_result,
|
|
HtmlParserSessionResult::Script(handle)
|
|
if self.peek_pending_custom_element_construction_handoff_placeholder()
|
|
== Some(handle.node_id())
|
|
);
|
|
let paused_for_stylesheet = !paused_for_custom_element
|
|
&& matches!(
|
|
&tokenizer_result,
|
|
HtmlParserSessionResult::Script(handle)
|
|
if self.peek_pending_blocking_stylesheet_pause() == Some(handle.node_id())
|
|
);
|
|
// Claim this yield before discovery and script-preparation callbacks
|
|
// can register another stylesheet pause on the same live document.
|
|
let claimed_stylesheet_pause = paused_for_stylesheet
|
|
.then(|| self.pop_pending_blocking_stylesheet_pause())
|
|
.flatten();
|
|
let result = match tokenizer_result {
|
|
HtmlParserSessionResult::OwnerInterrupted => RawParserStep::OwnerInterrupted,
|
|
HtmlParserSessionResult::Script(handle) if paused_for_custom_element => {
|
|
debug_assert_eq!(
|
|
self.peek_pending_custom_element_construction_handoff_placeholder(),
|
|
Some(handle.node_id())
|
|
);
|
|
RawParserStep::CustomElementConstruction
|
|
}
|
|
HtmlParserSessionResult::Script(handle) if paused_for_stylesheet => {
|
|
debug_assert_eq!(claimed_stylesheet_pause, Some(handle.node_id()));
|
|
RawParserStep::BlockingStylesheet(handle.node_id())
|
|
}
|
|
HtmlParserSessionResult::Script(handle) => RawParserStep::Script(handle.node_id()),
|
|
HtmlParserSessionResult::InputDrained => RawParserStep::InputDrained,
|
|
};
|
|
let discovered_async_prefetch_candidate_node_ids =
|
|
self.drain_discovered_async_prefetch_candidates();
|
|
let discovered_preload_link_candidate_node_ids =
|
|
self.drain_discovered_preload_link_candidates();
|
|
let discovered_blocking_stylesheet_inputs =
|
|
self.drain_discovered_blocking_stylesheet_inputs();
|
|
let captured_blocking_stylesheet_signatures =
|
|
self.captured_blocking_stylesheet_signatures();
|
|
let discovered_async_prefetch_script_positions =
|
|
discovered_async_prefetch_candidate_node_ids
|
|
.iter()
|
|
.copied()
|
|
.map(|node_id| (node_id, self.parser_script_position(node_id)))
|
|
.collect::<HashMap<_, _>>();
|
|
let handoff_parser_position = match result {
|
|
RawParserStep::Script(node_id) => Some(self.parser_script_position(node_id)),
|
|
RawParserStep::OwnerInterrupted
|
|
| RawParserStep::CustomElementConstruction
|
|
| RawParserStep::BlockingStylesheet(_)
|
|
| RawParserStep::InputDrained => None,
|
|
};
|
|
|
|
let defer_script_preparation = self.defer_script_preparation
|
|
&& self
|
|
.parser
|
|
.sink()
|
|
.borrow_target()
|
|
.has_runtime_dom_consumer();
|
|
if defer_script_preparation {
|
|
// A speculative fetch is distinct from preparing a parser script.
|
|
// The live owner admits async execution only after its checkpoint;
|
|
// ordinary preload discovery continues on the separate preload lane.
|
|
let result = match result {
|
|
RawParserStep::OwnerInterrupted => {
|
|
ParserPumpStep::Yield(ParserYield::OwnerInterrupted)
|
|
}
|
|
RawParserStep::Script(node_id) => {
|
|
let target = self.parser.sink().borrow_target();
|
|
let (start_line, start_column) =
|
|
target.script_start_position(node_id).unwrap_or((0, 0));
|
|
ParserPumpStep::Yield(ParserYield::ScriptPreparation(Box::new(
|
|
ParserScriptPreparationRequest {
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
position: handoff_parser_position.expect("script has parser position"),
|
|
needs_microtask_checkpoint: target
|
|
.read_is_html_element_named(node_id, "script"),
|
|
blocking_signatures_before: captured_blocking_stylesheet_signatures,
|
|
},
|
|
)))
|
|
}
|
|
RawParserStep::BlockingStylesheet(node_id) => {
|
|
assert_eq!(claimed_stylesheet_pause, Some(node_id));
|
|
ParserPumpStep::Yield(ParserYield::BlockingStylesheet(
|
|
ParserBlockingStylesheetPause { node_id },
|
|
))
|
|
}
|
|
RawParserStep::CustomElementConstruction => {
|
|
ParserPumpStep::Yield(ParserYield::CustomElementConstruction(Box::new(
|
|
self.pop_pending_custom_element_construction_handoff()
|
|
.expect("custom element boundary has a construction handoff"),
|
|
)))
|
|
}
|
|
RawParserStep::InputDrained => ParserPumpStep::InputDrained,
|
|
};
|
|
return ParserPumpOutcome {
|
|
result,
|
|
discovered_async_prefetch_scripts: Vec::new(),
|
|
discovered_preload_link_candidates: discovered_preload_link_candidate_node_ids,
|
|
discovered_blocking_stylesheet_inputs,
|
|
};
|
|
}
|
|
|
|
let (
|
|
result,
|
|
discovered_async_prefetch_scripts,
|
|
discovered_preload_link_candidates,
|
|
discovered_blocking_stylesheet_inputs,
|
|
) = {
|
|
let target = self.parser.sink().borrow_target();
|
|
let handoff_preparation = match result {
|
|
RawParserStep::Script(node_id) => {
|
|
let (start_line, start_column) =
|
|
target.script_start_position(node_id).unwrap_or((0, 0));
|
|
Some((
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
captured_blocking_stylesheet_signatures.clone(),
|
|
prepare_parser_script(&*target, node_id, handoff_parser_position),
|
|
))
|
|
}
|
|
RawParserStep::OwnerInterrupted
|
|
| RawParserStep::CustomElementConstruction
|
|
| RawParserStep::BlockingStylesheet(_)
|
|
| RawParserStep::InputDrained => None,
|
|
};
|
|
let discovered_async_prefetch_scripts = discovered_async_prefetch_candidate_node_ids
|
|
.iter()
|
|
.filter_map(|node_id| {
|
|
if let Some((handoff_node_id, _, _, _, preparation)) =
|
|
handoff_preparation.as_ref()
|
|
&& handoff_node_id == node_id
|
|
{
|
|
preparation.async_prefetch_script()
|
|
} else {
|
|
prepare_parser_script(
|
|
&*target,
|
|
*node_id,
|
|
discovered_async_prefetch_script_positions
|
|
.get(node_id)
|
|
.copied(),
|
|
)
|
|
.async_prefetch_script()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let blocking_classic_handoff_node_id =
|
|
handoff_preparation
|
|
.as_ref()
|
|
.and_then(|(node_id, _, _, _, preparation)| {
|
|
preparation.is_blocking_classic().then_some(*node_id)
|
|
});
|
|
drop(target);
|
|
|
|
let result = if let (
|
|
RawParserStep::Script(_),
|
|
Some((node_id, start_line, start_column, blocking_signatures_before, preparation)),
|
|
) = (result, handoff_preparation)
|
|
{
|
|
ParserPumpStep::Yield(ParserYield::Script(Box::new(preparation.into_handoff(
|
|
node_id,
|
|
start_line,
|
|
start_column,
|
|
blocking_signatures_before,
|
|
))))
|
|
} else if let RawParserStep::BlockingStylesheet(node_id) = result {
|
|
assert_eq!(
|
|
claimed_stylesheet_pause,
|
|
Some(node_id),
|
|
"tokenizer stylesheet yield must consume its matching pending parser pause"
|
|
);
|
|
ParserPumpStep::Yield(ParserYield::BlockingStylesheet(
|
|
ParserBlockingStylesheetPause { node_id },
|
|
))
|
|
} else if let RawParserStep::OwnerInterrupted = result {
|
|
ParserPumpStep::Yield(ParserYield::OwnerInterrupted)
|
|
} else if let Some(handoff) = self.pop_pending_custom_element_construction_handoff() {
|
|
ParserPumpStep::Yield(ParserYield::CustomElementConstruction(Box::new(handoff)))
|
|
} else {
|
|
ParserPumpStep::InputDrained
|
|
};
|
|
debug_assert_eq!(
|
|
blocking_classic_handoff_node_id.is_some(),
|
|
matches!(
|
|
result,
|
|
ParserPumpStep::Yield(ParserYield::Script(ref handoff))
|
|
if matches!(handoff.as_ref(), ParserScriptHandoff::BlockingClassic { .. })
|
|
)
|
|
);
|
|
(
|
|
result,
|
|
discovered_async_prefetch_scripts,
|
|
discovered_preload_link_candidate_node_ids,
|
|
discovered_blocking_stylesheet_inputs,
|
|
)
|
|
};
|
|
|
|
ParserPumpOutcome {
|
|
result,
|
|
discovered_async_prefetch_scripts,
|
|
discovered_preload_link_candidates,
|
|
discovered_blocking_stylesheet_inputs,
|
|
}
|
|
}
|
|
|
|
pub fn finish_live_runtime_dom_sink_parser(self) -> ParserFinishDiscoverySignals {
|
|
let HtmlTreeSinkStream {
|
|
parser,
|
|
script_input: _,
|
|
parser_script_positions: _,
|
|
next_parser_script_position: _,
|
|
defer_script_preparation: _,
|
|
} = self;
|
|
parser.finish_live_runtime_dom_sink_parser()
|
|
}
|
|
|
|
pub fn with_stylesheet_blocking_read_view<R>(
|
|
&self,
|
|
f: impl FnOnce(&dyn StylesheetBlockingReadView) -> R,
|
|
) -> R {
|
|
let target = self.parser.sink().borrow_target();
|
|
f(&*target)
|
|
}
|
|
|
|
pub fn snapshot_parser_stream_document(&self) -> NativeDom {
|
|
self.parser.sink().snapshot_parser_stream_document()
|
|
}
|
|
|
|
pub fn snapshot_parser_stream_dom_host(&self) -> DomHost {
|
|
self.parser.sink().snapshot_parser_stream_dom_host()
|
|
}
|
|
|
|
pub fn take_parser_stream_null_custom_element_registry_elements(&self) -> Vec<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.take_parser_stream_null_custom_element_registry_elements()
|
|
}
|
|
|
|
pub fn take_parser_stream_dom_host(&self) -> DomHost {
|
|
self.parser.sink().take_parser_stream_dom_host()
|
|
}
|
|
|
|
pub fn restore_parser_stream_dom_host(&self, dom_host: DomHost) {
|
|
self.parser.sink().restore_parser_stream_dom_host(dom_host);
|
|
}
|
|
|
|
pub(super) fn enter_runtime_dom_sinks_parse_step(&self, sinks: ParserRuntimeDomSinks) {
|
|
self.parser.sink().enter_runtime_dom_sinks_parse_step(sinks);
|
|
}
|
|
|
|
pub fn clear_runtime_dom_sinks_after_parse_step(&self) {
|
|
self.parser
|
|
.sink()
|
|
.clear_runtime_dom_sinks_after_parse_step()
|
|
}
|
|
|
|
pub fn replace_parser_stream_document(&self, document: NativeDom) {
|
|
self.parser.sink().replace_parser_stream_document(document);
|
|
}
|
|
|
|
pub fn drain_ready_parser_scripts(&self) -> Vec<NativeNodeId> {
|
|
self.parser.sink().drain_ready_parser_scripts()
|
|
}
|
|
|
|
pub fn drain_discovered_async_prefetch_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.drain_discovered_async_prefetch_candidates()
|
|
}
|
|
|
|
pub fn drain_discovered_preload_link_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.drain_discovered_preload_link_candidates()
|
|
}
|
|
|
|
pub fn drain_discovered_parser_meta_csp_candidates(&self) -> Vec<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.drain_discovered_parser_meta_csp_candidates()
|
|
}
|
|
|
|
pub fn note_defined_autonomous_custom_element(&self, local_name: &str) {
|
|
self.parser
|
|
.sink()
|
|
.note_defined_autonomous_custom_element(local_name);
|
|
}
|
|
|
|
pub fn drain_pending_custom_element_construction_handoffs(
|
|
&self,
|
|
) -> Vec<crate::html::ParserCustomElementConstructionHandoff> {
|
|
self.parser
|
|
.sink()
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
}
|
|
|
|
fn peek_pending_custom_element_construction_handoff_placeholder(&self) -> Option<NativeNodeId> {
|
|
self.parser
|
|
.sink()
|
|
.pending_custom_element_construction_handoff_placeholder()
|
|
}
|
|
|
|
fn pop_pending_custom_element_construction_handoff(
|
|
&self,
|
|
) -> Option<crate::html::ParserCustomElementConstructionHandoff> {
|
|
self.parser
|
|
.sink()
|
|
.pop_pending_custom_element_construction_handoff()
|
|
}
|
|
|
|
fn peek_pending_blocking_stylesheet_pause(&self) -> Option<NativeNodeId> {
|
|
self.parser.sink().pending_blocking_stylesheet_pause()
|
|
}
|
|
|
|
fn pop_pending_blocking_stylesheet_pause(&self) -> Option<NativeNodeId> {
|
|
self.parser.sink().pop_pending_blocking_stylesheet_pause()
|
|
}
|
|
|
|
fn drain_discovered_blocking_stylesheet_inputs(
|
|
&self,
|
|
) -> Vec<moli_stylesheet_blocking::DocumentOwnedBlockingStylesheetDiscoveryInput> {
|
|
self.parser
|
|
.sink()
|
|
.drain_discovered_blocking_stylesheet_inputs()
|
|
}
|
|
|
|
fn captured_blocking_stylesheet_signatures(
|
|
&self,
|
|
) -> std::collections::HashSet<DocumentBlockingStylesheetSignature> {
|
|
self.parser.sink().captured_blocking_stylesheet_signatures()
|
|
}
|
|
|
|
pub fn mark_script_already_started(&self, node_id: NativeNodeId) {
|
|
self.parser.sink().mark_script_already_started(node_id);
|
|
}
|
|
|
|
pub fn has_buffered_input(&self) -> bool {
|
|
self.parser.has_buffered_input()
|
|
}
|
|
|
|
pub fn buffered_input_len(&self) -> usize {
|
|
self.parser.buffered_input_len()
|
|
}
|
|
|
|
pub fn snapshot_buffered_input(&self) -> String {
|
|
self.parser.snapshot_buffered_input()
|
|
}
|
|
|
|
pub fn finish(self) -> NativeDom {
|
|
self.finish_dom_host().into_dom()
|
|
}
|
|
|
|
pub fn finish_dom_host(self) -> DomHost {
|
|
self.finish_dom_host_with_discovery_signals().0
|
|
}
|
|
|
|
pub(super) fn finish_dom_host_with_discovery_signals(
|
|
self,
|
|
) -> (DomHost, ParserFinishDiscoverySignals) {
|
|
let HtmlTreeSinkStream {
|
|
parser,
|
|
script_input: _,
|
|
parser_script_positions: _,
|
|
next_parser_script_position: _,
|
|
defer_script_preparation: _,
|
|
} = self;
|
|
let mut target = parser.finish();
|
|
let signals = ParserFinishDiscoverySignals {
|
|
parser_created_null_registry_elements: target
|
|
.take_parser_stream_null_custom_element_registry_elements(),
|
|
discovered_preload_link_candidates: target.drain_discovered_preload_link_candidates(),
|
|
discovered_parser_meta_csp_candidates: target
|
|
.drain_discovered_parser_meta_csp_candidates(),
|
|
discovered_blocking_stylesheet_inputs: target
|
|
.drain_discovered_blocking_stylesheet_inputs(),
|
|
};
|
|
(target.finish_dom_host(), signals)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{ParserScriptPreparation, prepare_parser_script};
|
|
use crate::{
|
|
DocumentStream, HtmlParser, ParserPlanningReadView, ParserPumpStep, ParserScriptHandoff,
|
|
ParserYield, PreparedScript, ScriptSource,
|
|
};
|
|
use moli_dom::native::{DomHost, DomMutationEffects, NativeDom, NativeNodeId, Node};
|
|
use moli_page_types::{ScriptKind, ScriptMode, ScriptSkipReason, ScriptSourceKind};
|
|
use moli_stylesheet_blocking::DocumentBlockingStylesheetSignature;
|
|
use url::Url;
|
|
|
|
struct TestMutationEffectCollector<'a> {
|
|
host: *mut DomHost,
|
|
effects: &'a mut DomMutationEffects,
|
|
interrupt_when_body_text_is: Option<&'a str>,
|
|
}
|
|
|
|
impl crate::ParserDomReadConsumer for TestMutationEffectCollector<'_> {
|
|
fn node_exists(&mut self, node_id: NativeNodeId) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.node(node_id).is_some()
|
|
}
|
|
|
|
fn is_connected(&mut self, node_id: NativeNodeId) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.is_connected(node_id)
|
|
}
|
|
|
|
fn is_text_node(&mut self, node_id: NativeNodeId) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }
|
|
.node(node_id)
|
|
.and_then(moli_dom::native::Node::as_text)
|
|
.is_some()
|
|
}
|
|
|
|
fn owner_document(&mut self, node_id: NativeNodeId) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.owner_document_handle(node_id)
|
|
}
|
|
|
|
fn parent_node(&mut self, node_id: NativeNodeId) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }
|
|
.node(node_id)
|
|
.and_then(moli_dom::native::Node::parent_node)
|
|
}
|
|
|
|
fn previous_sibling(&mut self, node_id: NativeNodeId) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }
|
|
.node(node_id)
|
|
.and_then(moli_dom::native::Node::prev_sibling)
|
|
}
|
|
|
|
fn last_child(&mut self, node_id: NativeNodeId) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }
|
|
.node(node_id)
|
|
.and_then(moli_dom::native::Node::last_child)
|
|
}
|
|
|
|
fn child_handles(&mut self, node_id: NativeNodeId) -> Vec<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.child_handles(node_id).collect()
|
|
}
|
|
|
|
fn document_body_handle_for_document(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.document_body_handle_for_document(document_handle)
|
|
}
|
|
|
|
fn template_contents_handle(&mut self, node_id: NativeNodeId) -> Option<NativeNodeId> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.parser_template_contents_handle(node_id)
|
|
}
|
|
|
|
fn is_html_element_named(&mut self, node_id: NativeNodeId, local_name: &str) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }
|
|
.dom()
|
|
.is_html_element_named(node_id, local_name)
|
|
}
|
|
|
|
fn is_external_async_classic_candidate(&mut self, node_id: NativeNodeId) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let Some(element) = (unsafe { &*self.host })
|
|
.node(node_id)
|
|
.and_then(moli_dom::native::Node::as_element)
|
|
else {
|
|
return false;
|
|
};
|
|
if !element.is_script_element() {
|
|
return false;
|
|
}
|
|
if element.script_source_attribute().is_none() || element.attribute("async").is_none() {
|
|
return false;
|
|
}
|
|
if element.is_html_script() && element.attribute("nomodule").is_some() {
|
|
return false;
|
|
}
|
|
let Some(script_type) = element.attribute("type") else {
|
|
return true;
|
|
};
|
|
if script_type.is_empty() {
|
|
return true;
|
|
}
|
|
moli_script::classify_script_kind(Some(script_type)) == ScriptKind::Classic
|
|
}
|
|
|
|
fn parser_script_read(&mut self, node_id: NativeNodeId) -> Option<crate::ParserScriptRead> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
<DomHost as ParserPlanningReadView>::parser_script_read(unsafe { &*self.host }, node_id)
|
|
}
|
|
|
|
fn stylesheet_element(
|
|
&mut self,
|
|
node_id: NativeNodeId,
|
|
) -> Option<moli_stylesheet_blocking::StylesheetElementRead> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
(unsafe { &*self.host })
|
|
.node(node_id)
|
|
.and_then(moli_stylesheet_blocking::StylesheetElementRead::from_node)
|
|
}
|
|
|
|
fn text_content(&mut self, node_id: NativeNodeId) -> Option<String> {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &*self.host }.text_content(node_id)
|
|
}
|
|
}
|
|
|
|
impl crate::ParserDomMutationConsumer for TestMutationEffectCollector<'_> {
|
|
fn apply_parser_dom_mutation(&mut self, mutation: crate::ParserDomMutation) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let effects = mutation.apply_to_dom_host(unsafe { &mut *self.host });
|
|
self.effects.merge(effects);
|
|
}
|
|
|
|
fn create_element_for_document_without_attributes(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
local_name: String,
|
|
namespace: String,
|
|
prefix: Option<String>,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_element_without_attributes_for_document(
|
|
document_handle,
|
|
local_name,
|
|
namespace,
|
|
prefix,
|
|
)
|
|
}
|
|
|
|
fn create_parser_element_for_document_without_attributes(
|
|
&mut self,
|
|
construction: &moli_dom::native::ParserConstruction,
|
|
document_handle: NativeNodeId,
|
|
local_name: String,
|
|
namespace: String,
|
|
prefix: Option<String>,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
construction.create_element(
|
|
unsafe { &mut *self.host },
|
|
document_handle,
|
|
local_name,
|
|
namespace,
|
|
prefix,
|
|
)
|
|
}
|
|
|
|
fn add_attrs_if_missing_for_parser(
|
|
&mut self,
|
|
node_id: NativeNodeId,
|
|
attrs: Vec<moli_dom::native::Attribute>,
|
|
) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.add_attrs_if_missing_for_parser(node_id, attrs);
|
|
}
|
|
|
|
fn create_text_node(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
text: String,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_text_node_for_document(document_handle, &text)
|
|
}
|
|
|
|
fn create_comment(&mut self, document_handle: NativeNodeId, text: String) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_comment_for_document(document_handle, &text)
|
|
}
|
|
|
|
fn create_processing_instruction(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
target: String,
|
|
data: String,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_processing_instruction_for_document(
|
|
document_handle,
|
|
&target,
|
|
&data,
|
|
)
|
|
}
|
|
|
|
fn create_cdata_section(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
data: String,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_cdata_section_for_document(document_handle, &data)
|
|
}
|
|
|
|
fn create_document_type(
|
|
&mut self,
|
|
document_handle: NativeNodeId,
|
|
name: String,
|
|
public_id: String,
|
|
system_id: String,
|
|
) -> NativeNodeId {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.create_document_type_for_document(
|
|
document_handle,
|
|
&name,
|
|
&public_id,
|
|
&system_id,
|
|
)
|
|
}
|
|
|
|
fn prepend_text_to_text_node(
|
|
&mut self,
|
|
node_id: NativeNodeId,
|
|
text: String,
|
|
) -> DomMutationEffects {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let host = unsafe { &mut *self.host };
|
|
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
|
|
return DomMutationEffects::default();
|
|
};
|
|
let mut merged = text;
|
|
merged.push_str(previous.data());
|
|
host.set_text_content_effects(node_id, &merged)
|
|
}
|
|
|
|
fn append_text_to_text_node(
|
|
&mut self,
|
|
node_id: NativeNodeId,
|
|
text: String,
|
|
) -> DomMutationEffects {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let host = unsafe { &mut *self.host };
|
|
let Some(previous) = host.node(node_id).and_then(Node::as_text) else {
|
|
return DomMutationEffects::default();
|
|
};
|
|
let mut merged = previous.data().to_owned();
|
|
merged.push_str(&text);
|
|
host.set_text_content_effects(node_id, &merged)
|
|
}
|
|
|
|
fn push_parse_error(&mut self, error: String) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.push_parse_error(error);
|
|
}
|
|
|
|
fn set_html_quirks_mode_for_parser(
|
|
&mut self,
|
|
quirks_mode: html5ever::tree_builder::QuirksMode,
|
|
) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.set_html_quirks_mode_for_parser(quirks_mode);
|
|
}
|
|
|
|
fn mark_script_already_started_for_parser(&mut self, node_id: NativeNodeId) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let _ = unsafe { &mut *self.host }.set_script_already_started(node_id, true);
|
|
}
|
|
|
|
fn mark_unclosed_form_control_for_parser(&mut self, node_id: NativeNodeId) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let _ = unsafe { &mut *self.host }.set_blocks_form_submission(node_id, true);
|
|
}
|
|
|
|
fn finish_parsing_children(
|
|
&mut self,
|
|
construction: &moli_dom::native::ParserConstruction,
|
|
node_id: NativeNodeId,
|
|
) {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
let effects = construction.finish_children(unsafe { &mut *self.host }, node_id);
|
|
self.effects.merge(effects);
|
|
}
|
|
|
|
fn attach_declarative_shadow_for_parser(
|
|
&mut self,
|
|
host_id: NativeNodeId,
|
|
template_id: NativeNodeId,
|
|
attrs: Vec<moli_dom::native::Attribute>,
|
|
) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.attach_declarative_shadow_for_parser(
|
|
host_id,
|
|
template_id,
|
|
&attrs,
|
|
)
|
|
}
|
|
|
|
fn associate_parser_form_owner(
|
|
&mut self,
|
|
target: NativeNodeId,
|
|
form: NativeNodeId,
|
|
) -> bool {
|
|
// SAFETY: the test keeps the DomHost alive for this parser pump step.
|
|
unsafe { &mut *self.host }.associate_parser_form_owner(target, form)
|
|
}
|
|
}
|
|
|
|
impl crate::ParserMutationEffectConsumer for TestMutationEffectCollector<'_> {
|
|
fn consume_parser_mutation_effects(&mut self, effects: DomMutationEffects) {
|
|
self.effects.merge(effects);
|
|
}
|
|
|
|
fn finish_parser_dom_mutations(&mut self) -> std::ops::ControlFlow<()> {
|
|
let Some(expected) = self.interrupt_when_body_text_is else {
|
|
return std::ops::ControlFlow::Continue(());
|
|
};
|
|
// SAFETY: the test owns the DomHost throughout this parser pump.
|
|
let host = unsafe { &*self.host };
|
|
let text = host
|
|
.document_body_handle_for_document(host.document_handle())
|
|
.and_then(|body| host.text_content(body));
|
|
if text.as_deref() == Some(expected) {
|
|
// Model a one-shot callback notification, not a level-triggered
|
|
// owner query. The adapter must retain it across input chunks.
|
|
self.interrupt_when_body_text_is = None;
|
|
std::ops::ControlFlow::Break(())
|
|
} else {
|
|
std::ops::ControlFlow::Continue(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn first_script_handle(html: &str) -> (NativeDom, NativeNodeId) {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.test/").expect("test url should parse"),
|
|
html.to_owned(),
|
|
);
|
|
let handle = document
|
|
.script_handles()
|
|
.into_iter()
|
|
.next()
|
|
.expect("fixture should contain a script");
|
|
(document, handle)
|
|
}
|
|
|
|
fn prepare_parser_blocking_classic_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
) -> Option<PreparedScript> {
|
|
match prepare_parser_script(document, node_id, None) {
|
|
ParserScriptPreparation::BlockingClassic(script) => Some(script),
|
|
ParserScriptPreparation::AsyncPostParse(_)
|
|
| ParserScriptPreparation::NonAsyncPostParse(_)
|
|
| ParserScriptPreparation::ImportMap(_)
|
|
| ParserScriptPreparation::NoExecution(_)
|
|
| ParserScriptPreparation::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
fn prepare_parser_async_post_parse_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
) -> Option<PreparedScript> {
|
|
match prepare_parser_script(document, node_id, None) {
|
|
ParserScriptPreparation::AsyncPostParse(script) => Some(script),
|
|
ParserScriptPreparation::BlockingClassic(_)
|
|
| ParserScriptPreparation::NonAsyncPostParse(_)
|
|
| ParserScriptPreparation::ImportMap(_)
|
|
| ParserScriptPreparation::NoExecution(_)
|
|
| ParserScriptPreparation::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
fn prepare_parser_non_async_post_parse_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
) -> Option<PreparedScript> {
|
|
match prepare_parser_script(document, node_id, None) {
|
|
ParserScriptPreparation::NonAsyncPostParse(script) => Some(script),
|
|
ParserScriptPreparation::BlockingClassic(_)
|
|
| ParserScriptPreparation::AsyncPostParse(_)
|
|
| ParserScriptPreparation::ImportMap(_)
|
|
| ParserScriptPreparation::NoExecution(_)
|
|
| ParserScriptPreparation::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
fn prepare_parser_post_parse_script(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
) -> Option<PreparedScript> {
|
|
match prepare_parser_script(document, node_id, None) {
|
|
ParserScriptPreparation::AsyncPostParse(script)
|
|
| ParserScriptPreparation::NonAsyncPostParse(script) => Some(script),
|
|
ParserScriptPreparation::BlockingClassic(_)
|
|
| ParserScriptPreparation::ImportMap(_)
|
|
| ParserScriptPreparation::NoExecution(_)
|
|
| ParserScriptPreparation::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
fn prepare_parser_import_map(
|
|
document: &impl ParserPlanningReadView,
|
|
node_id: NativeNodeId,
|
|
) -> Option<crate::PreparedImportMap> {
|
|
match prepare_parser_script(document, node_id, None) {
|
|
ParserScriptPreparation::ImportMap(import_map) => Some(import_map),
|
|
ParserScriptPreparation::BlockingClassic(_)
|
|
| ParserScriptPreparation::AsyncPostParse(_)
|
|
| ParserScriptPreparation::NonAsyncPostParse(_)
|
|
| ParserScriptPreparation::NoExecution(_)
|
|
| ParserScriptPreparation::PreparationFailure(_) => None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_feed_continues_past_definitive_encoding_indicator() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
stream.feed("<!doctype html><meta charset='utf-8'><body><p>after meta</p>");
|
|
|
|
let document = stream.snapshot_parser_stream_document();
|
|
assert_eq!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.len(),
|
|
1,
|
|
"the advisory encoding indicator must not leave decoded input buffered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_pump_continues_past_definitive_encoding_indicator() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome =
|
|
stream.pump_parser_step("<!doctype html><meta charset='utf-8'><body><p>after meta</p>");
|
|
|
|
assert!(matches!(outcome.result, ParserPumpStep::InputDrained));
|
|
let document = stream.snapshot_parser_stream_document();
|
|
assert_eq!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.len(),
|
|
1,
|
|
"the parser pump must consume the tail after an encoding indicator"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_finish_continues_past_definitive_encoding_indicator() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let outcome = stream.pump_parser_step(concat!(
|
|
"<!doctype html><script>window.ready = true;</script>",
|
|
"<meta charset='utf-8'><p>after meta</p>"
|
|
));
|
|
assert!(matches!(
|
|
outcome.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
|
|
let document = stream.finish();
|
|
assert_eq!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.len(),
|
|
1,
|
|
"finishing must consume the buffered tail after an encoding indicator"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_blocking_classic_script_accepts_inline_classic() {
|
|
let (document, handle) = first_script_handle("<script>window.ran = true;</script>");
|
|
assert_eq!(
|
|
document
|
|
.node(handle)
|
|
.and_then(Node::as_element)
|
|
.map(|element| element.script_text_internal_slot()),
|
|
Some("window.ran = true;")
|
|
);
|
|
let prepared = prepare_parser_blocking_classic_script(&document, handle)
|
|
.expect("inline classic parser script should be prepared");
|
|
assert_eq!(prepared.kind, ScriptKind::Classic);
|
|
assert_eq!(prepared.mode, ScriptMode::Normal);
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::Inline);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_svg_script_uses_shared_script_identity_and_href_source() {
|
|
let (document, handle) =
|
|
first_script_handle("<svg><script async href=\"/svg-script.js\"></script></svg>");
|
|
let element = document
|
|
.node(handle)
|
|
.and_then(Node::as_element)
|
|
.expect("SVG script element");
|
|
assert!(element.is_script_element());
|
|
assert_eq!(element.wrapper_prototype_name(), "SVGScriptElement");
|
|
assert_eq!(document.script_src(handle), Some("/svg-script.js"));
|
|
assert!(element.script_parser_inserted_for_prepare());
|
|
|
|
let prepared = prepare_parser_async_post_parse_script(&document, handle)
|
|
.expect("external async SVG script should be prepared");
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::External);
|
|
assert_eq!(prepared.url.as_str(), "https://example.test/svg-script.js");
|
|
}
|
|
|
|
#[test]
|
|
fn parser_finished_svg_script_seeds_trusted_source_state() {
|
|
let (document, handle) =
|
|
first_script_handle("<svg><script type=unknown>window.svgReady = true;</script></svg>");
|
|
let element = document
|
|
.node(handle)
|
|
.and_then(Node::as_element)
|
|
.expect("SVG script element");
|
|
|
|
assert_eq!(
|
|
element.script_text_internal_slot(),
|
|
"window.svgReady = true;"
|
|
);
|
|
assert!(!element.script_children_changed_by_api());
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_blocking_classic_script_rejects_external_defer() {
|
|
let (document, handle) = first_script_handle("<script defer src=\"/defer.js\"></script>");
|
|
assert!(prepare_parser_blocking_classic_script(&document, handle).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_blocking_classic_script_rejects_external_async() {
|
|
let (document, handle) = first_script_handle("<script async src=\"/async.js\"></script>");
|
|
assert!(prepare_parser_blocking_classic_script(&document, handle).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_blocking_classic_script_rejects_module_and_importmap() {
|
|
let (module_document, module_handle) =
|
|
first_script_handle("<script type=\"module\">export {};</script>");
|
|
assert!(prepare_parser_blocking_classic_script(&module_document, module_handle).is_none());
|
|
|
|
let (importmap_document, importmap_handle) = first_script_handle(
|
|
"<script type=\"importmap\">{\"imports\":{\"x\":\"/x.js\"}}</script>",
|
|
);
|
|
assert!(
|
|
prepare_parser_blocking_classic_script(&importmap_document, importmap_handle).is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_blocking_classic_script_rejects_already_started_script() {
|
|
let (mut document, handle) = first_script_handle("<script>window.ran = true;</script>");
|
|
let Some(node) = document.node_mut(handle) else {
|
|
panic!("script node should exist");
|
|
};
|
|
let Some(element) = node.data_mut().as_element_mut() else {
|
|
panic!("script handle should resolve to an element");
|
|
};
|
|
let _ = element.set_script_already_started(true);
|
|
|
|
assert!(prepare_parser_blocking_classic_script(&document, handle).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_post_parse_script_accepts_external_defer() {
|
|
let (document, handle) = first_script_handle("<script defer src=\"/defer.js\"></script>");
|
|
let prepared = prepare_parser_post_parse_script(&document, handle)
|
|
.expect("external defer parser script should be prepared");
|
|
assert_eq!(prepared.kind, ScriptKind::Classic);
|
|
assert_eq!(prepared.mode, ScriptMode::Defer);
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::External);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_post_parse_script_accepts_external_async() {
|
|
let (document, handle) = first_script_handle("<script async src=\"/async.js\"></script>");
|
|
let prepared = prepare_parser_post_parse_script(&document, handle)
|
|
.expect("external async parser script should be prepared");
|
|
assert_eq!(prepared.kind, ScriptKind::Classic);
|
|
assert_eq!(prepared.mode, ScriptMode::Async);
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::External);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_preparation_separates_module_execution_from_import_map_registration() {
|
|
let (module_document, module_handle) =
|
|
first_script_handle("<script type=\"module\">window.moduleReady = true;</script>");
|
|
let prepared = prepare_parser_post_parse_script(&module_document, module_handle)
|
|
.expect("inline module parser script should be prepared");
|
|
assert_eq!(prepared.kind, ScriptKind::Module);
|
|
assert_eq!(prepared.mode, ScriptMode::ModuleDefer);
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::Inline);
|
|
|
|
let (importmap_document, importmap_handle) = first_script_handle(
|
|
"<script type=\"importmap\" nonce=\"map-nonce\">{\"imports\":{\"x\":\"/x.js\"}}</script>",
|
|
);
|
|
let prepared = prepare_parser_import_map(&importmap_document, importmap_handle)
|
|
.expect("inline importmap should produce a registration payload");
|
|
assert_eq!(prepared.nonce.as_deref(), Some("map-nonce"));
|
|
assert!(matches!(
|
|
prepared.source,
|
|
crate::PreparedImportMapSource::Inline(ref source)
|
|
if source == "{\"imports\":{\"x\":\"/x.js\"}}"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_handoff_preserves_defer_modes_for_owner_acceptance() {
|
|
let classic_stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let classic = classic_stream
|
|
.pump_parser_step("<script defer src='/defer.js'></script>")
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(classic)) = classic else {
|
|
panic!("expected classic defer parser handoff");
|
|
};
|
|
let ParserScriptHandoff::NonAsyncPostParse { script, .. } = *classic else {
|
|
panic!("classic defer must use the parser-owned post-parse handoff");
|
|
};
|
|
assert_eq!(script.kind, ScriptKind::Classic);
|
|
assert_eq!(script.mode, ScriptMode::Defer);
|
|
|
|
let module_stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let module = module_stream
|
|
.pump_parser_step("<script type='module'>export const value = 1;</script>")
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(module)) = module else {
|
|
panic!("expected module-defer parser handoff");
|
|
};
|
|
let ParserScriptHandoff::NonAsyncPostParse { script, .. } = *module else {
|
|
panic!("module defer must use the parser-owned post-parse handoff");
|
|
};
|
|
assert_eq!(script.kind, ScriptKind::Module);
|
|
assert_eq!(script.mode, ScriptMode::ModuleDefer);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_assigns_stable_positions_to_shadow_root_scripts() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let first = stream
|
|
.pump_parser_step(
|
|
"<div><template shadowrootmode='open'><script>window.first = true</script><script>window.second = true</script></template></div>",
|
|
)
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(first)) = first else {
|
|
panic!("expected first shadow-root script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic { script: first, .. } = *first else {
|
|
panic!("expected parser-blocking classic shadow-root script");
|
|
};
|
|
assert_eq!(first.position, 0);
|
|
assert!(
|
|
matches!(first.source, ScriptSource::Inline(ref source) if source.contains("window.first"))
|
|
);
|
|
|
|
let second = stream.pump_parser_step("").result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(second)) = second else {
|
|
panic!("expected second shadow-root script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic { script: second, .. } = *second else {
|
|
panic!("expected second parser-blocking classic shadow-root script");
|
|
};
|
|
assert_eq!(second.position, 1);
|
|
assert!(
|
|
matches!(second.source, ScriptSource::Inline(ref source) if source.contains("window.second"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_keeps_ordinary_template_scripts_inert() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let handoff = stream
|
|
.pump_parser_step(
|
|
"<template><script>window.templateScriptRan = true</script></template>",
|
|
)
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = handoff else {
|
|
panic!("expected template script handoff");
|
|
};
|
|
let ParserScriptHandoff::NoExecution { outcome, .. } = *handoff else {
|
|
panic!("ordinary template script must remain inert");
|
|
};
|
|
assert_eq!(
|
|
outcome.element_state_transition(),
|
|
crate::ParserScriptElementStateTransition::ConsumeParserInserted { force_async: true }
|
|
);
|
|
let (position, mode, run) = outcome.into_parts();
|
|
assert_eq!(position, 0);
|
|
assert_eq!(mode, ScriptMode::Normal);
|
|
assert!(matches!(
|
|
run,
|
|
Some(run)
|
|
if matches!(
|
|
run.outcome(),
|
|
moli_page_types::ScriptRunOutcome::Skipped(
|
|
ScriptSkipReason::NotInMainDocument
|
|
)
|
|
)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_handoff_keeps_import_maps_out_of_executable_script_lanes() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let result = stream
|
|
.pump_parser_step(
|
|
"<script type='importmap' async defer>{\"imports\":{\"x\":\"/x.js\"}}</script>",
|
|
)
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = result else {
|
|
panic!("expected import-map parser handoff");
|
|
};
|
|
let ParserScriptHandoff::ImportMap { import_map, .. } = *handoff else {
|
|
panic!("import map must not use an executable parser handoff");
|
|
};
|
|
assert!(matches!(
|
|
import_map.source,
|
|
crate::PreparedImportMapSource::Inline(source)
|
|
if source.contains("\"x\":\"/x.js\"")
|
|
));
|
|
|
|
let external_stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let result = external_stream
|
|
.pump_parser_step("<script type='importmap' src='/map.json'></script>")
|
|
.result;
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = result else {
|
|
panic!("expected external import-map handoff");
|
|
};
|
|
assert!(matches!(
|
|
*handoff,
|
|
ParserScriptHandoff::ImportMap {
|
|
import_map: crate::PreparedImportMap {
|
|
source: crate::PreparedImportMapSource::ExternalUnsupported,
|
|
..
|
|
},
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_post_parse_script_accepts_async_module_via_async_lane() {
|
|
let (document, handle) = first_script_handle(
|
|
"<script type=\"module\" async>window.moduleReady = true;</script>",
|
|
);
|
|
let prepared = prepare_parser_async_post_parse_script(&document, handle)
|
|
.expect("async module parser script should use async post-parse lane");
|
|
assert_eq!(prepared.kind, ScriptKind::Module);
|
|
assert_eq!(prepared.mode, ScriptMode::Async);
|
|
assert_eq!(prepared.source_kind, ScriptSourceKind::Inline);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_non_async_post_parse_script_rejects_async_modules() {
|
|
let (document, handle) = first_script_handle(
|
|
"<script type=\"module\" async>window.moduleReady = true;</script>",
|
|
);
|
|
assert!(
|
|
prepare_parser_non_async_post_parse_script(&document, handle).is_none(),
|
|
"async module script should not use non-async post-parse path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_post_parse_script_rejects_inline_and_normal_classic() {
|
|
let (inline_document, inline_handle) =
|
|
first_script_handle("<script>window.ran = true;</script>");
|
|
assert!(prepare_parser_post_parse_script(&inline_document, inline_handle).is_none());
|
|
|
|
let (normal_external_document, normal_external_handle) =
|
|
first_script_handle("<script src=\"/normal.js\"></script>");
|
|
assert!(
|
|
prepare_parser_post_parse_script(&normal_external_document, normal_external_handle)
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_src_parser_script_handoff_is_unprepared_not_fetchable() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome =
|
|
stream.pump_parser_step("<!doctype html><html><head><script src=\"\"></script>");
|
|
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::PreparationFailure { failure, .. } = *handoff else {
|
|
panic!("empty src must not become a blocking classic fetch");
|
|
};
|
|
assert!(failure.is_external_source_failure());
|
|
assert_eq!(
|
|
failure.element_state_transition(),
|
|
crate::ParserScriptElementStateTransition::MarkAlreadyStarted
|
|
);
|
|
let (position, mode, message) = failure.into_parts();
|
|
assert_eq!(position, 0);
|
|
assert_eq!(mode, ScriptMode::Normal);
|
|
assert_eq!(message, "empty script src is not fetchable");
|
|
}
|
|
|
|
#[test]
|
|
fn internal_parser_script_preparation_failure_is_not_an_external_source_error() {
|
|
let failure = crate::ParserScriptPreparationFailure::new(
|
|
0,
|
|
ScriptMode::Normal,
|
|
"document URL missing during parser script preparation".to_owned(),
|
|
);
|
|
assert!(!failure.is_external_source_failure());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_src_parser_script_handoff_preserves_preparation_failure() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream
|
|
.pump_parser_step("<!doctype html><html><head><script src=\"http://[::1\"></script>");
|
|
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::PreparationFailure { failure, .. } = *handoff else {
|
|
panic!("invalid src must stay a failed parser preparation");
|
|
};
|
|
assert!(failure.is_external_source_failure());
|
|
assert_eq!(
|
|
failure.element_state_transition(),
|
|
crate::ParserScriptElementStateTransition::MarkAlreadyStarted
|
|
);
|
|
let (position, mode, failure) = failure.into_parts();
|
|
assert_eq!(position, 0);
|
|
assert_eq!(mode, ScriptMode::Normal);
|
|
assert!(
|
|
failure.contains("failed to resolve script src"),
|
|
"invalid src failure should survive the parser boundary"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_script_handoff_only_exposes_nonceable_nonces() {
|
|
fn handoff_nonce(markup: &str) -> Option<String> {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let outcome = stream.pump_parser_step(markup);
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser script handoff for {markup:?}");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic { script, .. } = *handoff else {
|
|
panic!("expected blocking classic script for {markup:?}");
|
|
};
|
|
script.fetch_metadata.nonce
|
|
}
|
|
|
|
assert_eq!(
|
|
handoff_nonce("<script nonce=abc>safe()</script>"),
|
|
Some("abc".to_owned()),
|
|
"ordinary parser script nonce should remain usable"
|
|
);
|
|
for markup in [
|
|
"<script attribute<script nonce=abc>blocked()</script>",
|
|
"<script data-marker='value<StYlE' nonce=abc>blocked()</script>",
|
|
"<script data-marker='value<LiNk' nonce=abc>blocked()</script>",
|
|
"<script duplicate duplicate nonce=abc>blocked()</script>",
|
|
"<svg><script duplicate duplicate nonce=abc>blocked()</script></svg>",
|
|
] {
|
|
assert_eq!(
|
|
handoff_nonce(markup),
|
|
None,
|
|
"nonnonceable parser script must not expose its nonce: {markup:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_script_handoff_uses_html5ever_line_with_unknown_column_across_input_chunks() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let first = stream.pump_parser_step("<!doctype html>\r\n <scr");
|
|
assert!(matches!(first.result, ParserPumpStep::InputDrained));
|
|
let second = stream.pump_parser_step("ipt>window.ran = true;</script>");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = second.result else {
|
|
panic!("expected parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected blocking classic handoff");
|
|
};
|
|
|
|
assert_eq!(start_line, 2);
|
|
assert_eq!(start_column, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn nonempty_parser_inserted_input_permanently_degrades_source_locations() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outer = stream.pump_parser_step(
|
|
"<!doctype html>\n<script>outer()</script>\n <script>original()</script>",
|
|
);
|
|
assert!(matches!(
|
|
outer.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
let inserted = stream.pump_parser_inserted_step("\n <script>inserted()</script>\n");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = inserted.result else {
|
|
panic!("expected inserted parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected inserted blocking classic handoff");
|
|
};
|
|
assert_eq!((start_line, start_column), (0, 0));
|
|
|
|
assert!(matches!(
|
|
stream.pump_parser_step("").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
|
|
let original = stream.pump_parser_step("");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = original.result else {
|
|
panic!("expected original parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected original blocking classic handoff");
|
|
};
|
|
assert_eq!((start_line, start_column), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn blocked_nested_writes_keep_each_input_at_its_insertion_depth() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outer = stream.pump_parser_step("<script>outer()</script>e");
|
|
assert!(matches!(
|
|
outer.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
let nested = stream.pump_parser_inserted_step("<script src='nested.js'></script>r");
|
|
assert!(matches!(
|
|
nested.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
|
|
assert!(stream.append_to_current_inserted_input("k"));
|
|
stream.append_to_end("d".to_owned());
|
|
stream
|
|
.script_input_session()
|
|
.enqueue_script_input_html("wo".to_owned());
|
|
|
|
assert_eq!(stream.snapshot_pending_input(), "worked");
|
|
}
|
|
|
|
#[test]
|
|
fn parser_token_crossing_inserted_and_original_input_stays_unknown() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outer = stream.pump_parser_step(
|
|
"<script>outer()</script>ipt>cross()</script>\n<script>original()</script>",
|
|
);
|
|
assert!(matches!(
|
|
outer.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
assert!(matches!(
|
|
stream.pump_parser_inserted_step("<scr").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
|
|
let cross_source = stream.pump_parser_step("");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = cross_source.result else {
|
|
panic!("expected cross-source parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected cross-source blocking classic handoff");
|
|
};
|
|
assert_eq!((start_line, start_column), (0, 0));
|
|
|
|
let original = stream.pump_parser_step("");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = original.result else {
|
|
panic!("expected following original parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected following original blocking classic handoff");
|
|
};
|
|
assert_eq!((start_line, start_column), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_inserted_character_reference_chunks_degrade_without_disrupting_parsing() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let markup =
|
|
"<a href='?a=b&c=d&a0b=c©=1¬i=n¬=in¬in=∉¬&;& &'>Link</a>";
|
|
|
|
for character in markup.chars() {
|
|
assert!(matches!(
|
|
stream
|
|
.pump_parser_inserted_step(&character.to_string())
|
|
.result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
}
|
|
|
|
let outcome = stream.pump_parser_step("original text\n<script>original()</script>");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected original parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected original blocking classic handoff");
|
|
};
|
|
|
|
assert_eq!((start_line, start_column), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_original_source_uses_html5ever_line_without_tracking_utf16_columns() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step("\n😀<script>original()</script>");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected blocking classic handoff");
|
|
};
|
|
|
|
assert_eq!((start_line, start_column), (2, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_parser_inserted_input_preserves_following_line_location() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outer =
|
|
stream.pump_parser_step("<script>outer()</script>\n<script>original()</script>");
|
|
assert!(matches!(
|
|
outer.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
assert!(matches!(
|
|
stream.pump_parser_inserted_step("").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
|
|
let original = stream.pump_parser_step("");
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = original.result else {
|
|
panic!("expected original parser script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
start_line,
|
|
start_column,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected original blocking classic handoff");
|
|
};
|
|
assert_eq!((start_line, start_column), (2, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_surfaces_inline_svg_script_handoff() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><body><svg><script>window.svgReady = true;</script></svg><p>late</p>",
|
|
);
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected SVG script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
node_id, script, ..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected inline SVG script to use the blocking classic lane");
|
|
};
|
|
let document = stream.snapshot_parser_stream_document();
|
|
let element = document
|
|
.node(node_id)
|
|
.and_then(Node::as_element)
|
|
.expect("SVG script element");
|
|
|
|
assert_eq!(element.wrapper_prototype_name(), "SVGScriptElement");
|
|
assert_eq!(script.source_kind, ScriptSourceKind::Inline);
|
|
assert_eq!(
|
|
document.script_text(node_id).as_deref(),
|
|
Some("window.svgReady = true;")
|
|
);
|
|
assert!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.is_empty(),
|
|
"parser must not consume content after the SVG script handoff"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_surfaces_self_closing_external_svg_script_handoff() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><body><svg><script href='/self-closing.js'/></svg><p>late</p>",
|
|
);
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected self-closing SVG script handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic { script, .. } = *handoff else {
|
|
panic!("expected external SVG script to use the blocking classic lane");
|
|
};
|
|
|
|
assert_eq!(script.source_kind, ScriptSourceKind::External);
|
|
assert_eq!(script.url.as_str(), "https://example.test/self-closing.js");
|
|
let document = stream.snapshot_parser_stream_document();
|
|
assert!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.is_empty(),
|
|
"parser must pause immediately after a self-closing SVG script"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_does_not_execute_svg_script_popped_by_an_ancestor_end_tag() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><body><svg><script>window.mustStayInert = true;</svg><p>after</p>",
|
|
);
|
|
|
|
assert!(matches!(outcome.result, ParserPumpStep::InputDrained));
|
|
let document = stream.snapshot_parser_stream_document();
|
|
assert_eq!(
|
|
document
|
|
.elements_by_tag_name(document.document_node_id(), "p", false)
|
|
.len(),
|
|
1,
|
|
"foreign stack reconciliation should still restore the HTML parent"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn data_block_handoff_consumes_parser_inserted_prepare_state() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let result = stream
|
|
.pump_parser_step("<script type='application/json'>{\"ok\":true}</script>")
|
|
.result;
|
|
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = result else {
|
|
panic!("expected data-block parser handoff");
|
|
};
|
|
let ParserScriptHandoff::NoExecution { outcome, .. } = *handoff else {
|
|
panic!("data block must remain non-executable");
|
|
};
|
|
assert_eq!(
|
|
outcome.element_state_transition(),
|
|
crate::ParserScriptElementStateTransition::ConsumeParserInserted { force_async: true }
|
|
);
|
|
assert!(matches!(
|
|
outcome.run(),
|
|
Some(run)
|
|
if matches!(
|
|
run.outcome(),
|
|
moli_page_types::ScriptRunOutcome::Skipped(
|
|
ScriptSkipReason::UnsupportedType(_)
|
|
)
|
|
)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_records_custom_element_construction_handoff_candidates() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
stream.note_defined_autonomous_custom_element("x-ready");
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><body><x-ready id='first' data-probe='yes'></x-ready><x-late></x-late>",
|
|
);
|
|
let ParserPumpStep::Yield(ParserYield::CustomElementConstruction(handoff)) = outcome.result
|
|
else {
|
|
panic!("expected parser pump to surface custom element construction handoff");
|
|
};
|
|
let handoff = &*handoff;
|
|
assert_eq!(handoff.local_name, "x-ready");
|
|
assert_eq!(handoff.namespace, "http://www.w3.org/1999/xhtml");
|
|
assert_eq!(handoff.prefix, None);
|
|
assert_eq!(handoff.parent_at_creation, None);
|
|
assert_eq!(handoff.owner_document.index(), 0);
|
|
assert!(handoff.placeholder.index() > handoff.owner_document.index());
|
|
assert!(
|
|
handoff
|
|
.attributes
|
|
.iter()
|
|
.any(|attribute| attribute.name() == "id" && attribute.value() == "first")
|
|
);
|
|
assert!(
|
|
handoff
|
|
.attributes
|
|
.iter()
|
|
.any(|attribute| attribute.name() == "data-probe" && attribute.value() == "yes")
|
|
);
|
|
assert!(
|
|
stream
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
.is_empty(),
|
|
"handoff queue should drain exactly once"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_surfaces_multiple_custom_element_handoffs_one_at_a_time() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
stream.note_defined_autonomous_custom_element("x-ready");
|
|
|
|
let first = stream.pump_parser_step(
|
|
"<!doctype html><body><x-ready id='a'></x-ready><x-ready id='b'></x-ready>",
|
|
);
|
|
let ParserPumpStep::Yield(ParserYield::CustomElementConstruction(first_handoff)) =
|
|
first.result
|
|
else {
|
|
panic!("expected first custom element construction handoff");
|
|
};
|
|
assert_eq!(first_handoff.local_name, "x-ready");
|
|
assert!(
|
|
first_handoff
|
|
.attributes
|
|
.iter()
|
|
.any(|attribute| attribute.name() == "id" && attribute.value() == "a")
|
|
);
|
|
|
|
let second = stream.pump_parser_step("");
|
|
let ParserPumpStep::Yield(ParserYield::CustomElementConstruction(second_handoff)) =
|
|
second.result
|
|
else {
|
|
panic!("expected second custom element construction handoff");
|
|
};
|
|
assert_eq!(second_handoff.local_name, "x-ready");
|
|
assert!(
|
|
second_handoff
|
|
.attributes
|
|
.iter()
|
|
.any(|attribute| attribute.name() == "id" && attribute.value() == "b")
|
|
);
|
|
|
|
assert!(matches!(
|
|
stream.pump_parser_step("").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_custom_element_handoff_pauses_before_following_sibling_tokens() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
stream.note_defined_autonomous_custom_element("x-ready");
|
|
|
|
let first = stream.pump_parser_step(
|
|
"<!doctype html><body><x-ready id='a'></x-ready><x-late id='later'></x-late>",
|
|
);
|
|
let ParserPumpStep::Yield(ParserYield::CustomElementConstruction(first_handoff)) =
|
|
first.result
|
|
else {
|
|
panic!("expected custom element construction handoff");
|
|
};
|
|
assert_eq!(first_handoff.local_name, "x-ready");
|
|
|
|
let snapshot = stream.snapshot_parser_stream_document();
|
|
assert!(
|
|
snapshot.node(first_handoff.placeholder).is_some(),
|
|
"placeholder should exist at the handoff boundary"
|
|
);
|
|
assert!(
|
|
snapshot
|
|
.nodes()
|
|
.iter()
|
|
.all(|node| node.local_name() != Some("x-late")),
|
|
"parser must not consume following sibling tokens before custom element construction handoff"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_consumes_blocking_stylesheet_pause_before_resuming_tokenizer() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let first = stream.pump_parser_step(concat!(
|
|
"<!doctype html><body>",
|
|
"<link rel=stylesheet href='/slow.css'>",
|
|
"\n<script>window.afterStylesheet = true;</script>",
|
|
"<footer id=after>after</footer>",
|
|
"</body>"
|
|
));
|
|
let ParserPumpStep::Yield(ParserYield::BlockingStylesheet(pause)) = first.result else {
|
|
panic!("expected body stylesheet parser pause");
|
|
};
|
|
let paused_document = stream.snapshot_parser_stream_document();
|
|
assert!(
|
|
paused_document.node(pause.node_id).is_some(),
|
|
"stylesheet owner should exist at the parser pause boundary"
|
|
);
|
|
assert!(
|
|
paused_document
|
|
.elements_by_tag_name(paused_document.document_node_id(), "script", false)
|
|
.is_empty(),
|
|
"parser must not consume the following script start tag before the stylesheet settles"
|
|
);
|
|
assert!(
|
|
paused_document
|
|
.elements_by_tag_name(paused_document.document_node_id(), "footer", false)
|
|
.is_empty(),
|
|
"parser must retain the tail after the stylesheet pause"
|
|
);
|
|
|
|
let second = stream.pump_parser_step("");
|
|
assert!(
|
|
matches!(second.result, ParserPumpStep::Yield(ParserYield::Script(_))),
|
|
"resuming after the stylesheet pause should reach the following script"
|
|
);
|
|
|
|
assert!(matches!(
|
|
stream.pump_parser_step("").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
let completed_document = stream.snapshot_parser_stream_document();
|
|
assert_eq!(
|
|
completed_document
|
|
.elements_by_tag_name(completed_document.document_node_id(), "footer", false)
|
|
.len(),
|
|
1,
|
|
"parser should consume the retained tail after the script handoff"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_encoding_indicator_preserves_stylesheet_pause() {
|
|
for attributes in [
|
|
"charset=utf-8",
|
|
"charset=windows-1252",
|
|
"charset=unknown-encoding",
|
|
"http-equiv=content-type content='text/css; charset=utf-8'",
|
|
] {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
let tail = "\n<script>window.afterStylesheet = true;</script><p>café</p>";
|
|
let first = stream.pump_parser_step(&format!(
|
|
"<!doctype html><body><link rel=stylesheet href='/slow.css' {attributes}>{tail}"
|
|
));
|
|
assert!(
|
|
matches!(
|
|
first.result,
|
|
ParserPumpStep::Yield(ParserYield::BlockingStylesheet(_))
|
|
),
|
|
"encoding notification must not bypass the stylesheet pause: {attributes}"
|
|
);
|
|
assert_eq!(first.discovered_blocking_stylesheet_inputs.len(), 1);
|
|
assert_eq!(stream.snapshot_pending_input(), tail);
|
|
|
|
let second = stream.pump_parser_step("");
|
|
assert!(matches!(
|
|
second.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
assert_eq!(stream.snapshot_pending_input(), "<p>café</p>");
|
|
assert!(matches!(
|
|
stream.pump_parser_step("").result,
|
|
ParserPumpStep::InputDrained
|
|
));
|
|
let document = stream.finish();
|
|
let paragraphs = document.elements_by_tag_name(document.document_node_id(), "p", false);
|
|
assert_eq!(paragraphs.len(), 1);
|
|
assert_eq!(
|
|
document.text_content(paragraphs[0]).as_deref(),
|
|
Some("café")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_feed_consumes_custom_element_handoff_without_runtime_owner() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
stream.note_defined_autonomous_custom_element("x-ready");
|
|
|
|
stream.feed("<!doctype html><body><x-ready id='a'></x-ready><x-late id='later'></x-late>");
|
|
|
|
let snapshot = stream.snapshot_parser_stream_document();
|
|
assert!(
|
|
stream
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
.is_empty(),
|
|
"non-pump parsing has no runtime owner to consume parser-side handoffs"
|
|
);
|
|
assert!(
|
|
snapshot
|
|
.nodes()
|
|
.iter()
|
|
.any(|node| node.local_name() == Some("x-ready")),
|
|
"defined custom element token should still be parsed"
|
|
);
|
|
assert!(
|
|
snapshot
|
|
.nodes()
|
|
.iter()
|
|
.any(|node| node.local_name() == Some("x-late")),
|
|
"non-pump feed should continue parsing later sibling tokens"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_does_not_record_custom_element_handoff_without_runtime_definition() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page.html").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><body><x-ready id='first'></x-ready><x-late></x-late>",
|
|
);
|
|
assert!(matches!(outcome.result, ParserPumpStep::InputDrained));
|
|
assert!(
|
|
stream
|
|
.drain_pending_custom_element_construction_handoffs()
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_post_parse_script_rejects_data_block_and_already_started() {
|
|
let (data_block_document, data_block_handle) =
|
|
first_script_handle("<script type=\"application/json\">{\"ok\":true}</script>");
|
|
assert!(
|
|
prepare_parser_post_parse_script(&data_block_document, data_block_handle).is_none()
|
|
);
|
|
|
|
let (mut document, handle) =
|
|
first_script_handle("<script defer src=\"/defer.js\"></script>");
|
|
let Some(node) = document.node_mut(handle) else {
|
|
panic!("script node should exist");
|
|
};
|
|
let Some(element) = node.data_mut().as_element_mut() else {
|
|
panic!("script handle should resolve to an element");
|
|
};
|
|
let _ = element.set_script_already_started(true);
|
|
assert!(prepare_parser_post_parse_script(&document, handle).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_parser_helpers_can_read_from_live_dom_host() {
|
|
let (document, blocking_handle) =
|
|
first_script_handle("<script>window.ran = true;</script>");
|
|
let host = DomHost::from_dom(document);
|
|
let prepared = prepare_parser_blocking_classic_script(&host, blocking_handle)
|
|
.expect("live dom host should support parser-blocking planning reads");
|
|
assert_eq!(prepared.mode, ScriptMode::Normal);
|
|
assert_eq!(prepared.kind, ScriptKind::Classic);
|
|
|
|
let (document, defer_handle) =
|
|
first_script_handle("<script defer src=\"/defer.js\"></script>");
|
|
let host = DomHost::from_dom(document);
|
|
let prepared = prepare_parser_non_async_post_parse_script(&host, defer_handle)
|
|
.expect("live dom host should support parser post-parse planning reads");
|
|
assert_eq!(prepared.mode, ScriptMode::Defer);
|
|
assert_eq!(prepared.kind, ScriptKind::Classic);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_caches_document_url_before_runtime_dom_takeover() {
|
|
let url = Url::parse("https://example.test/page.html").expect("test url");
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(url.clone());
|
|
let dom_host = stream.take_parser_stream_dom_host();
|
|
|
|
let cached_url = stream.with_stylesheet_blocking_read_view(|view| view.final_url_clone());
|
|
assert_eq!(
|
|
cached_url.as_ref(),
|
|
Some(&url),
|
|
"parser planning should retain document URL after bootstrap DOM is taken by runtime"
|
|
);
|
|
|
|
stream.restore_parser_stream_dom_host(dom_host);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_reports_style_import_blocker_before_classic_handoff() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page").expect("test url"),
|
|
);
|
|
|
|
let outcome = stream.pump_parser_step(
|
|
"<!doctype html><html><head><style>@import url('/style.css');</style><script src='/app.js'></script></head></html>",
|
|
);
|
|
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser-blocking classic handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
blocking_signatures_before,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected parser-blocking classic handoff");
|
|
};
|
|
|
|
let expected_url = Url::parse("https://example.test/style.css").unwrap();
|
|
assert_eq!(outcome.discovered_blocking_stylesheet_inputs.len(), 1);
|
|
assert!(
|
|
blocking_signatures_before
|
|
.iter()
|
|
.any(|signature| matches!(signature, DocumentBlockingStylesheetSignature::ParserCreatedStyleImport { urls } if urls == &vec![expected_url.clone()])),
|
|
"handoff should carry parser-created style import blocker signature"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_style_children_finish_at_close_and_eof() {
|
|
for (opening, closing) in [
|
|
("<html><head><style id='sheet'>", "</style>"),
|
|
("<html><body><svg><style id='sheet'>", "</style></svg>"),
|
|
("<html><head><style id='sheet'>", ""),
|
|
] {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page").unwrap(),
|
|
);
|
|
stream.pump_parser_step(&format!("{opening}p {{ col"));
|
|
let document = stream.snapshot_parser_stream_document().into_document();
|
|
let style =
|
|
document.elements_by_tag_name(document.document_node_id(), "style", false)[0];
|
|
assert!(
|
|
document
|
|
.node(style)
|
|
.and_then(Node::as_element)
|
|
.unwrap()
|
|
.style_children_parsing()
|
|
);
|
|
stream.pump_parser_step(&format!("or: blue; }}{closing}"));
|
|
let document = if closing.is_empty() {
|
|
stream.finish()
|
|
} else {
|
|
stream.snapshot_parser_stream_document().into_document()
|
|
};
|
|
assert!(
|
|
!document
|
|
.node(style)
|
|
.and_then(Node::as_element)
|
|
.unwrap()
|
|
.style_children_parsing()
|
|
);
|
|
assert_eq!(
|
|
document.text_content(style).as_deref(),
|
|
Some("p { color: blue; }")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_reports_split_connected_meta_csp_once_and_ignores_template_contents() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page").expect("test url"),
|
|
);
|
|
|
|
let first = stream.pump_parser_step(r#"<meta http-equiv="Content-Security-"#);
|
|
assert!(matches!(first.result, ParserPumpStep::InputDrained));
|
|
assert!(
|
|
stream
|
|
.drain_discovered_parser_meta_csp_candidates()
|
|
.is_empty(),
|
|
"an incomplete tag must not publish a parser policy checkpoint"
|
|
);
|
|
|
|
let second = stream.pump_parser_step(
|
|
r#"Policy" content="script-src 'self'">
|
|
<template><meta http-equiv="content-security-policy" content="script-src 'none'"></template>
|
|
<script src="/app.js"></script>"#,
|
|
);
|
|
assert!(matches!(
|
|
second.result,
|
|
ParserPumpStep::Yield(ParserYield::Script(_))
|
|
));
|
|
let candidates = stream.drain_discovered_parser_meta_csp_candidates();
|
|
assert_eq!(candidates.len(), 1);
|
|
assert!(
|
|
stream
|
|
.drain_discovered_parser_meta_csp_candidates()
|
|
.is_empty(),
|
|
"a parser checkpoint must be consumed exactly once"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_style_reports_parsing_finished_only_after_its_closing_tag() {
|
|
use moli_dom::native::DomStylesheetOwnerChangeKind;
|
|
|
|
for prefix in ["<!doctype html><head>", "<!doctype html><body><svg>"] {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/style-stream").unwrap(),
|
|
);
|
|
let mut host = stream.take_parser_stream_dom_host();
|
|
host.set_mutation_observer_records_enabled(true);
|
|
let mut effects = DomMutationEffects::default();
|
|
let mut consumer = TestMutationEffectCollector {
|
|
host: &mut host,
|
|
effects: &mut effects,
|
|
interrupt_when_body_text_is: None,
|
|
};
|
|
stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
&format!("{prefix}<style id=sheet>"),
|
|
&mut consumer,
|
|
);
|
|
let style = host.elements_by_tag_name(host.document_handle(), "style", true)[0];
|
|
*consumer.effects = DomMutationEffects::default();
|
|
// Separate feeds and line breaks both split tokenizer text. Neither
|
|
// may publish growing stylesheet prefixes to the renderer.
|
|
let chunk = ".rule { color: red; }\n".repeat(64);
|
|
for _ in 0..8 {
|
|
stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
&chunk,
|
|
&mut consumer,
|
|
);
|
|
assert!(consumer.effects.stylesheet_owners().changes().is_empty());
|
|
assert!(!consumer.effects.observer_records().records().is_empty());
|
|
*consumer.effects = DomMutationEffects::default();
|
|
}
|
|
stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
"</style>",
|
|
&mut consumer,
|
|
);
|
|
let changes = consumer.effects.stylesheet_owners().changes();
|
|
assert_eq!(changes.len(), 1);
|
|
assert_eq!(changes[0].owner(), style);
|
|
assert!(matches!(
|
|
changes[0].kind(),
|
|
DomStylesheetOwnerChangeKind::ParsingFinished
|
|
));
|
|
assert!(consumer.effects.observer_records().records().is_empty());
|
|
assert!(!host.is_style_element_parsing_children(style));
|
|
assert_eq!(host.text_content(style).unwrap(), chunk.repeat(8));
|
|
stream.restore_parser_stream_dom_host(host);
|
|
stream.finish();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parser_stream_reports_style_import_blocker_on_runtime_dom_sinks() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/page").expect("test url"),
|
|
);
|
|
let mut dom_host = stream.take_parser_stream_dom_host();
|
|
let ptr = &mut dom_host as *mut DomHost;
|
|
let mut effects = DomMutationEffects::default();
|
|
|
|
let outcome = {
|
|
let mut collector = TestMutationEffectCollector {
|
|
host: ptr,
|
|
effects: &mut effects,
|
|
interrupt_when_body_text_is: None,
|
|
};
|
|
stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
"<!doctype html><html><head><style>@import url('/style.css');</style><script src='/app.js'></script></head></html>",
|
|
&mut collector,
|
|
)
|
|
};
|
|
stream.restore_parser_stream_dom_host(dom_host);
|
|
|
|
let ParserPumpStep::Yield(ParserYield::Script(handoff)) = outcome.result else {
|
|
panic!("expected parser-blocking classic handoff");
|
|
};
|
|
let ParserScriptHandoff::BlockingClassic {
|
|
blocking_signatures_before,
|
|
..
|
|
} = *handoff
|
|
else {
|
|
panic!("expected parser-blocking classic handoff");
|
|
};
|
|
|
|
let expected_url = Url::parse("https://example.test/style.css").unwrap();
|
|
assert!(
|
|
effects.did_change(),
|
|
"runtime DOM sink pump should report runtime-visible mutation effects through an explicit sink"
|
|
);
|
|
assert_eq!(outcome.discovered_blocking_stylesheet_inputs.len(), 1);
|
|
assert!(
|
|
blocking_signatures_before
|
|
.iter()
|
|
.any(|signature| matches!(signature, DocumentBlockingStylesheetSignature::ParserCreatedStyleImport { urls } if urls == &vec![expected_url.clone()])),
|
|
"handoff should carry parser-created style import blocker signature through runtime DOM sinks step"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_callback_interruption_survives_character_tokens_and_input_boundaries() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/").unwrap(),
|
|
);
|
|
let mut host = stream.take_parser_stream_dom_host();
|
|
let mut effects = DomMutationEffects::default();
|
|
let mut consumer = TestMutationEffectCollector {
|
|
host: &mut host,
|
|
effects: &mut effects,
|
|
interrupt_when_body_text_is: Some("pause"),
|
|
};
|
|
let first = stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
"<!doctype html><body>pause",
|
|
&mut consumer,
|
|
);
|
|
assert!(consumer.interrupt_when_body_text_is.is_none());
|
|
assert!(matches!(first.result, ParserPumpStep::InputDrained));
|
|
let second = stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
"&<!-- between -->more<span>tail</span>",
|
|
&mut consumer,
|
|
);
|
|
assert!(matches!(
|
|
second.result,
|
|
ParserPumpStep::Yield(ParserYield::OwnerInterrupted)
|
|
));
|
|
assert_eq!(stream.snapshot_pending_input(), "tail</span>");
|
|
let resumed = stream
|
|
.pump_parser_step_with_runtime_dom_consumer_without_element_creation("", &mut consumer);
|
|
assert!(matches!(resumed.result, ParserPumpStep::InputDrained));
|
|
stream.restore_parser_stream_dom_host(host);
|
|
let document = stream.finish();
|
|
let spans = document.elements_by_tag_name(document.document_node_id(), "span", false);
|
|
assert_eq!(spans.len(), 1);
|
|
assert_eq!(document.text_content(spans[0]).as_deref(), Some("tail"));
|
|
}
|
|
|
|
#[test]
|
|
fn parser_callback_interruption_at_text_eof_does_not_invent_a_script_handoff() {
|
|
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
|
|
Url::parse("https://example.test/").unwrap(),
|
|
);
|
|
let mut host = stream.take_parser_stream_dom_host();
|
|
let mut effects = DomMutationEffects::default();
|
|
let mut consumer = TestMutationEffectCollector {
|
|
host: &mut host,
|
|
effects: &mut effects,
|
|
interrupt_when_body_text_is: Some("pause"),
|
|
};
|
|
let step = stream.pump_parser_step_with_runtime_dom_consumer_without_element_creation(
|
|
"<!doctype html><body>pause&am",
|
|
&mut consumer,
|
|
);
|
|
assert!(consumer.interrupt_when_body_text_is.is_none());
|
|
assert!(matches!(step.result, ParserPumpStep::InputDrained));
|
|
stream.restore_parser_stream_dom_host(host);
|
|
let document = stream.finish();
|
|
let bodies = document.elements_by_tag_name(document.document_node_id(), "body", false);
|
|
assert_eq!(
|
|
document.text_content(bodies[0]).as_deref(),
|
|
Some("pause&am")
|
|
);
|
|
}
|
|
}
|