Files
moli/moli-parser/src/session.rs
T

470 lines
17 KiB
Rust

use std::{
cell::{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 moli_script::script_element_nonce_is_nonceable;
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>,
// Retain a callback's Break until html5ever accepts a pause result. This is
// an undelivered notification, not a copy of the owner's current run state;
// it must survive both later Continue callbacks and input chunk boundaries.
deferred_owner_interruption: Cell<bool>,
}
impl EmbedderPausingTreeBuilder {
fn new(sink: DocumentSink, opts: TreeBuilderOpts) -> Self {
Self {
inner: TreeBuilder::new(sink, opts),
deferred_owner_interruption: Cell::new(false),
}
}
fn new_for_fragment(
sink: DocumentSink,
context_handle: ParseHandle,
opts: TreeBuilderOpts,
) -> Self {
Self {
inner: TreeBuilder::new_for_fragment(sink, context_handle, None, opts),
deferred_owner_interruption: Cell::new(false),
}
}
fn sink(&self) -> &DocumentSink {
&self.inner.sink
}
}
impl EmbedderPausingTreeBuilder {
fn process_token_before_callbacks(
&self,
token: Token,
line_number: u64,
) -> TokenSinkResult<ParseHandle> {
let current_script_nonceable = match &token {
Token::TagToken(tag)
if tag.kind == TagKind::StartTag && tag.name.as_ref() == "script" =>
{
let nonce = tag
.attrs
.iter()
.find(|attribute| {
attribute.name.ns.is_empty() && attribute.name.local.as_ref() == "nonce"
})
.map(|attribute| attribute.value.as_ref());
Some(script_element_nonce_is_nonceable(
nonce,
tag.had_duplicate_attributes,
tag.attrs
.iter()
.map(|attribute| (attribute.name.local.as_ref(), attribute.value.as_ref())),
))
}
_ => None,
};
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 previous_script_nonceable = self
.inner
.sink
.replace_current_script_nonceable(current_script_nonceable);
let result = self.inner.process_token(token, line_number);
self.inner
.sink
.replace_current_script_nonceable(previous_script_nonceable);
// Encoding notices are advisory for this already-decoded input. A tag
// such as <link rel=stylesheet charset=utf-8> can also require an
// embedder pause, which must be returned at this tag boundary. Leaving
// it pending would expose it on a later character or error token,
// where html5ever requires Continue and would otherwise panic.
if !matches!(
result,
TokenSinkResult::Continue | TokenSinkResult::EncodingIndicator(_)
) {
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));
}
result
}
}
impl TokenSink for EmbedderPausingTreeBuilder {
type Handle = TokenizerPause;
fn process_token(&self, token: Token, line_number: u64) -> TokenSinkResult<Self::Handle> {
// html5ever requires character, comment, doctype and EOF tokens to
// return Continue. A parser mutation callback may make a stylesheet or
// custom-element pause visible only after one of those tokens. Retain
// the sink-owned pause until a tag token can yield it safely.
let can_pause = matches!(&token, Token::TagToken(_));
let result = self.process_token_before_callbacks(token, line_number);
let current_owner_interruption = self.sink().finish_parser_dom_mutations().is_break();
let deferred_owner_interruption = self.deferred_owner_interruption.replace(false);
let owner_interrupted = current_owner_interruption || deferred_owner_interruption;
if !can_pause {
self.deferred_owner_interruption.set(owner_interrupted);
return TokenSinkResult::Continue;
}
if owner_interrupted {
// 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);
}
if matches!(
&result,
TokenSinkResult::Continue | TokenSinkResult::EncodingIndicator(_)
) {
if let Some(placeholder) = self
.sink()
.pending_custom_element_construction_handoff_placeholder()
{
// html5ever has no custom-element pause result. The stream
// distinguishes this handle using sink-owned state.
return TokenSinkResult::Script(TokenizerPause::Handoff(ParseHandle::new(
placeholder,
None,
)));
}
if let Some(stylesheet) = self.sink().pending_blocking_stylesheet_pause() {
// Query after mutation callbacks so the handle matches
// the queue entry consumed by the stream layer.
return TokenSinkResult::Script(TokenizerPause::Handoff(ParseHandle::new(
stylesheet, None,
)));
}
}
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();
sink.finish_construction();
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()
}
}