Files
moli/moli-parser/src/session.rs
ldm0 4f7c4ffc0b refactor(parser): share mutation effects and isolate tokenizer pauses
Share parser mutation notifications between the main document and child
frames, with each owner retaining its resource and lifecycle work. Update
stylesheet owners incrementally so parser mutations preserve CSSOM edits.
Keep tokenizer pause reasons separate from DOM handle identity.

Add main/iframe regressions for reaction and observer ordering, form
association, stylesheet visibility, CSSOM edits, and nested frame identity.

Validation: cargo fmt --all; strict workspace clippy; cargo nextest run
--no-fail-fast (18,118 passed, 13 skipped).
2026-09-17 01:11:29 +08:00

404 lines
14 KiB
Rust

use std::{cell::RefCell, rc::Rc};
use html5ever::{LocalName, Namespace, QualName};
use html5ever::{
ParseOpts,
tendril::StrTendril,
tokenizer::{
BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
},
tree_builder::{TreeBuilder, TreeBuilderOpts, TreeSink},
};
use markup5ever::TokenizerResult;
use moli_dom::native::NativeNodeId;
use super::{
html::{DocumentSink, ParseHandle, ParserFinishDiscoverySignals, ParserInputQueue},
html_input::InputStack,
live_target::ParserStreamHtmlTreeSinkTarget,
};
pub(super) struct HtmlTreeSinkSession {
pub(super) parser: HtmlParserSession,
pub(super) script_input: ParserInputQueue,
}
pub(super) struct HtmlParserSession {
tokenizer: Tokenizer<EmbedderPausingTreeBuilder>,
input: RefCell<InputStack>,
}
pub(super) enum HtmlParserSessionResult {
InputDrained,
OwnerInterrupted,
Script(ParseHandle),
}
// html5ever carries tokenizer pauses through Script(Handle). Keep the pause
// reason separate from the tree builder's DOM handles at this adapter boundary.
enum TokenizerPause {
Handoff(ParseHandle),
OwnerInterrupted,
}
struct EmbedderPausingTreeBuilder {
inner: TreeBuilder<ParseHandle, DocumentSink>,
}
impl EmbedderPausingTreeBuilder {
fn new(sink: DocumentSink, opts: TreeBuilderOpts) -> Self {
Self {
inner: TreeBuilder::new(sink, opts),
}
}
fn new_for_fragment(
sink: DocumentSink,
context_handle: ParseHandle,
opts: TreeBuilderOpts,
) -> Self {
Self {
inner: TreeBuilder::new_for_fragment(sink, context_handle, None, opts),
}
}
fn sink(&self) -> &DocumentSink {
&self.inner.sink
}
}
impl EmbedderPausingTreeBuilder {
fn process_token_before_callbacks(
&self,
token: Token,
line_number: u64,
) -> TokenSinkResult<ParseHandle> {
let in_foreign_content = self
.inner
.adjusted_current_node_present_but_not_in_html_namespace();
let foreign_end_tag = match &token {
Token::TagToken(tag) if in_foreign_content && tag.kind == TagKind::EndTag => {
Some(tag.name.clone())
}
_ => None,
};
let self_closing_foreign_start_tag = match &token {
Token::TagToken(tag)
if in_foreign_content && tag.kind == TagKind::StartTag && tag.self_closing =>
{
Some(tag.name.clone())
}
_ => None,
};
let result = self.inner.process_token(token, line_number);
if !matches!(result, TokenSinkResult::Continue) {
return result;
}
let svg_script_handoff = if let Some(local_name) = foreign_end_tag {
// html5ever truncates its foreign-content open-element stack
// directly, bypassing TreeSink::pop(). Keep the parser target's
// mirror synchronized and expose closed SVG scripts below.
self.inner
.sink
.note_foreign_end_tag_processed(local_name.as_ref())
} else if let Some(local_name) = self_closing_foreign_start_tag {
self.inner
.sink
.note_self_closing_foreign_element_processed(local_name.as_ref())
} else {
None
};
if let Some(script) = svg_script_handoff {
// html5ever 0.39 has an explicit FIXME for </script> in SVG and
// does not return TokenSinkResult::Script for it. Preserve the
// ordinary tokenizer pause contract at this narrow adapter.
return TokenSinkResult::Script(ParseHandle::new(script, None));
}
if let Some(placeholder) = self
.inner
.sink
.pending_custom_element_construction_handoff_placeholder()
{
// html5ever has no custom-element pause result. Moli interprets the
// script handoff handle as a custom-element handoff when the parser
// sink has a matching pending construction record.
return TokenSinkResult::Script(ParseHandle::new(placeholder, None));
}
if let Some(stylesheet) = self.inner.sink.pending_blocking_stylesheet_pause() {
// html5ever exposes one generic tokenizer-yield result. The stream
// layer distinguishes this parser-created stylesheet boundary from
// actual script and custom-element handoffs using sink-owned state.
return TokenSinkResult::Script(ParseHandle::new(stylesheet, None));
}
TokenSinkResult::Continue
}
}
impl TokenSink for EmbedderPausingTreeBuilder {
type Handle = TokenizerPause;
fn process_token(&self, token: Token, line_number: u64) -> TokenSinkResult<Self::Handle> {
let result = self.process_token_before_callbacks(token, line_number);
if self.sink().finish_parser_dom_mutations().is_break() {
// A nested parser invocation already handed its blocker to the
// owner. Stop this outer feed before it consumes another token.
return TokenSinkResult::Script(TokenizerPause::OwnerInterrupted);
}
match result {
TokenSinkResult::Continue => TokenSinkResult::Continue,
TokenSinkResult::Script(handle) => {
TokenSinkResult::Script(TokenizerPause::Handoff(handle))
}
TokenSinkResult::Plaintext => TokenSinkResult::Plaintext,
TokenSinkResult::RawData(kind) => TokenSinkResult::RawData(kind),
TokenSinkResult::EncodingIndicator(label) => TokenSinkResult::EncodingIndicator(label),
}
}
fn end(&self) {
self.inner.end();
let _ = self.sink().finish_parser_dom_mutations();
}
fn adjusted_current_node_present_but_not_in_html_namespace(&self) -> bool {
self.inner
.adjusted_current_node_present_but_not_in_html_namespace()
}
}
impl HtmlParserSession {
fn new(sink: DocumentSink, opts: ParseOpts) -> Self {
let tree_builder = EmbedderPausingTreeBuilder::new(sink, opts.tree_builder);
Self {
tokenizer: Tokenizer::new(tree_builder, opts.tokenizer),
input: RefCell::default(),
}
}
pub(super) fn new_fragment(
sink: DocumentSink,
opts: ParseOpts,
context_handle: ParseHandle,
context_element_allows_scripting: bool,
) -> Self {
let tree_builder =
EmbedderPausingTreeBuilder::new_for_fragment(sink, context_handle, opts.tree_builder);
let tokenizer_options = TokenizerOpts {
initial_state: Some(
tree_builder
.inner
.tokenizer_state_for_context_elem(context_element_allows_scripting),
),
..opts.tokenizer
};
Self {
tokenizer: Tokenizer::new(tree_builder, tokenizer_options),
input: RefCell::default(),
}
}
pub(super) fn process(&self, input: StrTendril) {
self.input.borrow_mut().push_back(input);
let input = self.input.borrow().current();
while let HtmlParserSessionResult::Script(_) =
feed_with_definitive_encoding(&self.tokenizer, &input)
{
// Non-pump callers intentionally parse through embedder pauses. They have no
// runtime owner to notify, so parser-side custom-element handoffs and
// blocking-stylesheet pauses must be discarded before continuing.
self.discard_parser_side_embedder_yield();
}
}
pub(super) fn push_back(&self, input: StrTendril) {
self.input.borrow_mut().push_back(input);
}
pub(super) fn begin_inserted_input(&self, input: StrTendril) {
if input.is_empty() {
return;
}
// Without segment provenance from html5ever, any later token can span
// inserted input and the original tail after this insertion frame is
// restored. Prefer permanent unknown locations over reporting
// plausible but incorrect document lines.
self.tokenizer.sink.sink().mark_source_positions_unknown();
self.input.borrow_mut().begin_inserted(input);
}
pub(super) fn append_to_current_inserted_input(&self, input: StrTendril) -> bool {
self.input.borrow_mut().append_to_current_inserted(input)
}
pub(super) fn append_at_current_insertion_point(&self, input: StrTendril) {
self.tokenizer.sink.sink().mark_source_positions_unknown();
self.input.borrow_mut().push_back(input);
}
pub(super) fn has_buffered_input(&self) -> bool {
self.input.borrow().has_input()
}
pub(super) fn buffered_input_len(&self) -> usize {
self.input.borrow().len()
}
pub(super) fn snapshot_buffered_input(&self) -> String {
self.input.borrow().snapshot()
}
pub(super) fn feed(&self) -> HtmlParserSessionResult {
let input = self.input.borrow().current();
let result = feed_with_definitive_encoding(&self.tokenizer, &input);
if matches!(result, HtmlParserSessionResult::InputDrained)
&& self.input.borrow().is_current(&input)
{
// The restored parent is intentionally consumed by the next parser
// step so each insertion depth keeps an explicit input boundary.
self.input.borrow_mut().restore_parent_if_current_empty();
}
result
}
pub(super) fn sink(&self) -> &DocumentSink {
self.tokenizer.sink.sink()
}
pub(super) fn finish(self) -> ParserStreamHtmlTreeSinkTarget {
let Self { tokenizer, input } = self;
let input_buffer = input.into_inner().into_buffer();
while let HtmlParserSessionResult::Script(_) =
feed_with_definitive_encoding(&tokenizer, &input_buffer)
{
tokenizer
.sink
.sink()
.pop_pending_custom_element_construction_handoff();
tokenizer
.sink
.sink()
.pop_pending_blocking_stylesheet_pause();
}
debug_assert!(input_buffer.is_empty());
tokenizer.sink.sink().begin_tree_builder_finish();
tokenizer.end();
tokenizer.sink.inner.sink.finish()
}
pub(super) fn finish_live_runtime_dom_sink_parser(self) -> ParserFinishDiscoverySignals {
let Self { tokenizer, input } = self;
let input_buffer = input.into_inner().into_buffer();
while let HtmlParserSessionResult::Script(_) =
feed_with_definitive_encoding(&tokenizer, &input_buffer)
{
tokenizer
.sink
.sink()
.pop_pending_custom_element_construction_handoff();
tokenizer
.sink
.sink()
.pop_pending_blocking_stylesheet_pause();
}
debug_assert!(input_buffer.is_empty());
tokenizer.sink.sink().begin_tree_builder_finish();
tokenizer.end();
let sink = tokenizer.sink.sink();
ParserFinishDiscoverySignals {
parser_created_null_registry_elements: sink
.take_parser_stream_null_custom_element_registry_elements(),
discovered_modulepreload_link_candidates: sink
.drain_discovered_modulepreload_link_candidates(),
discovered_parser_meta_csp_candidates: sink
.drain_discovered_parser_meta_csp_candidates(),
discovered_blocking_stylesheet_inputs: sink
.drain_discovered_blocking_stylesheet_inputs(),
}
}
fn discard_parser_side_embedder_yield(&self) {
self.tokenizer
.sink
.sink()
.pop_pending_custom_element_construction_handoff();
self.tokenizer
.sink
.sink()
.pop_pending_blocking_stylesheet_pause();
}
}
fn feed_with_definitive_encoding(
tokenizer: &Tokenizer<EmbedderPausingTreeBuilder>,
input_buffer: &BufferQueue,
) -> HtmlParserSessionResult {
loop {
match tokenizer.feed(input_buffer) {
// Moli resolves the document encoding from the response and byte-level
// meta prescan before creating this Unicode parser session. The tree
// builder cannot change that definitive decoding, so continue past its
// advisory notification without exposing a false parser pause.
TokenizerResult::EncodingIndicator(_) => {}
TokenizerResult::Done => return HtmlParserSessionResult::InputDrained,
TokenizerResult::Script(TokenizerPause::Handoff(handle)) => {
return HtmlParserSessionResult::Script(handle);
}
TokenizerResult::Script(TokenizerPause::OwnerInterrupted) => {
return HtmlParserSessionResult::OwnerInterrupted;
}
}
}
}
pub(super) fn new_html_tree_sink_session(
target: ParserStreamHtmlTreeSinkTarget,
scripting_enabled: bool,
) -> HtmlTreeSinkSession {
let sink = DocumentSink::new(target);
let parser = HtmlParserSession::new(sink, html_parse_opts_with_scripting(scripting_enabled));
let script_input = ParserInputQueue::default();
HtmlTreeSinkSession {
parser,
script_input,
}
}
pub(super) fn new_fragment_html_tree_sink_session(
target: ParserStreamHtmlTreeSinkTarget,
context_handle: NativeNodeId,
context_namespace: &str,
context_local_name: &str,
scripting_enabled: bool,
) -> HtmlTreeSinkSession {
let sink = DocumentSink::new(target);
let context = QualName::new(
None,
Namespace::from(context_namespace),
LocalName::from(context_local_name),
);
let context_handle = ParseHandle::new(context_handle, Some(Rc::new(context)));
let parser = HtmlParserSession::new_fragment(
sink,
html_parse_opts_with_scripting(scripting_enabled),
context_handle,
scripting_enabled,
);
let script_input = ParserInputQueue::default();
HtmlTreeSinkSession {
parser,
script_input,
}
}
pub(super) fn html_parse_opts_with_scripting(scripting_enabled: bool) -> ParseOpts {
ParseOpts {
tree_builder: TreeBuilderOpts {
scripting_enabled,
..TreeBuilderOpts::default()
},
..ParseOpts::default()
}
}