mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
Flush parser insertion reactions after the tree builder completes its current token so connected callbacks run before children without retaining parser borrows across JavaScript execution. Keep one DocumentParserSession and expose a restricted insertion handle for nested writes. Share input, suspension, cancellation, and close state so blocked writes remain queued and outer feeds stop when nested parsing blocks or replaces the document. Let the owning session finish deferred close and dispatch readiness transitions once. Reuse form-owner mutation detection for child parser insertions and add regressions for reaction order, form association, nested writes, gated external scripts, document replacement, and deferred close. Validation: cargo fmt --all; strict workspace Clippy; cargo nextest run --no-fail-fast (18,104 passed, 13 existing skips); release build.
251 lines
8.9 KiB
Rust
251 lines
8.9 KiB
Rust
//! Parser-owned adapters for the shared stylesheet blocking read contract.
|
|
//!
|
|
//! Shared types and discovery behavior live in `moli-stylesheet-blocking`
|
|
//! and are intentionally not re-exported from this crate.
|
|
|
|
use moli_dom::{NodeId, native::NativeNodeId};
|
|
use moli_stylesheet_blocking::{StylesheetBlockingReadView, StylesheetElementRead};
|
|
use url::Url;
|
|
|
|
use crate::ParserStreamDocumentSnapshot;
|
|
|
|
#[cfg(test)]
|
|
use moli_stylesheet_blocking::{
|
|
collect_document_owned_blocking_stylesheet_candidates,
|
|
collect_document_owned_blocking_stylesheet_nodes_before, connected_preload_like_link_url,
|
|
document_owned_blocking_stylesheet_candidate_for_node, preload_like_link_loads_stylesheet,
|
|
stylesheet_link_disposition,
|
|
};
|
|
|
|
impl StylesheetBlockingReadView for ParserStreamDocumentSnapshot {
|
|
fn stylesheet_element(&self, node_id: NativeNodeId) -> Option<StylesheetElementRead> {
|
|
self.node(node_id)
|
|
.and_then(StylesheetElementRead::from_node)
|
|
}
|
|
|
|
fn child_ids(&self, node_id: NativeNodeId) -> Vec<NativeNodeId> {
|
|
self.child_ids(node_id).collect()
|
|
}
|
|
|
|
fn text_content(&self, node_id: NativeNodeId) -> Option<String> {
|
|
self.text_content(node_id)
|
|
}
|
|
|
|
fn final_url_clone(&self) -> Option<Url> {
|
|
self.final_url().cloned()
|
|
}
|
|
|
|
fn document_base_url_clone(&self) -> Option<Url> {
|
|
ParserStreamDocumentSnapshot::document_base_url(self)
|
|
}
|
|
|
|
fn document_node_id(&self) -> NativeNodeId {
|
|
self.document_node_id()
|
|
}
|
|
|
|
fn document_order_stylesheet_candidate_ids_before(
|
|
&self,
|
|
target_node_id: Option<NodeId>,
|
|
) -> Vec<NativeNodeId> {
|
|
self.stylesheet_candidate_handles_before_in_tree_scope(
|
|
self.document_node_id(),
|
|
target_node_id.map(|node_id| NativeNodeId::new(node_id.index())),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
collect_document_owned_blocking_stylesheet_candidates,
|
|
collect_document_owned_blocking_stylesheet_nodes_before, connected_preload_like_link_url,
|
|
document_owned_blocking_stylesheet_candidate_for_node, preload_like_link_loads_stylesheet,
|
|
stylesheet_link_disposition,
|
|
};
|
|
use crate::{HtmlParser, ParserPumpStep};
|
|
use moli_dom::native::Node;
|
|
|
|
#[test]
|
|
fn document_owned_blocking_candidates_include_parser_created_style_imports() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
url::Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><style>@import url('/slow.css');</style><script defer>window.x = 1;</script></head></html>".to_owned(),
|
|
);
|
|
let script = document.script_handles()[0];
|
|
|
|
let candidates = collect_document_owned_blocking_stylesheet_candidates(&document);
|
|
let blockers = collect_document_owned_blocking_stylesheet_nodes_before(
|
|
&document,
|
|
moli_dom::NodeId::new(script.index()),
|
|
);
|
|
|
|
assert_eq!(candidates.len(), 1);
|
|
assert_eq!(
|
|
document_owned_blocking_stylesheet_candidate_for_node(
|
|
&document,
|
|
candidates[0].node_id(),
|
|
),
|
|
Some(candidates[0].clone()),
|
|
);
|
|
assert_eq!(blockers.len(), 1);
|
|
assert_eq!(blockers[0], candidates[0].node_id());
|
|
}
|
|
|
|
#[test]
|
|
fn parser_created_link_blocker_is_captured_before_link_processing_state_is_consumed() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let stream = parser.start_document(url::Url::parse("https://example.com/").unwrap());
|
|
stream.append_to_end(
|
|
"<!doctype html><html><head><link rel=stylesheet href='/slow.css'><script>window.x = 1;</script></head></html>".to_owned(),
|
|
);
|
|
let mut captured_blockers = Vec::new();
|
|
while stream.has_pending_input() {
|
|
let outcome = stream.pump_next_parser_step(0);
|
|
captured_blockers.extend(outcome.discovered_blocking_stylesheet_inputs);
|
|
if matches!(outcome.result, ParserPumpStep::InputDrained) && !stream.has_pending_input()
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
let document = stream.finish();
|
|
let script = document.script_handles()[0];
|
|
|
|
let blockers = collect_document_owned_blocking_stylesheet_nodes_before(
|
|
&document,
|
|
moli_dom::NodeId::new(script.index()),
|
|
);
|
|
|
|
assert_eq!(captured_blockers.len(), 1);
|
|
assert!(
|
|
blockers.is_empty(),
|
|
"the completed link must not remain eligible for a later parser-blocking rescan"
|
|
);
|
|
assert!(
|
|
document
|
|
.node(moli_dom::native::NativeNodeId::new(
|
|
captured_blockers[0].node_id().index(),
|
|
))
|
|
.is_some_and(|node| node.flags().parser_created())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stylesheet_link_resolves_against_the_processed_document_base_url() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
url::Url::parse("https://example.com/page/index.html").unwrap(),
|
|
"<!doctype html><html><head><base href=\"/assets/\"><link rel=\"stylesheet\" href=\"app.css\"></head></html>"
|
|
.to_owned(),
|
|
);
|
|
let head = document.document_head_handle().expect("head handle");
|
|
let link = document
|
|
.child_nodes(head)
|
|
.expect("head children")
|
|
.into_iter()
|
|
.find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("link"))
|
|
})
|
|
.expect("link handle");
|
|
|
|
let disposition =
|
|
stylesheet_link_disposition(&document, moli_dom::NodeId::new(link.index()))
|
|
.expect("stylesheet disposition");
|
|
assert_eq!(
|
|
disposition.url().as_str(),
|
|
"https://example.com/assets/app.css"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preload_like_link_stylesheet_filter_ignores_script_modulepreloads() {
|
|
assert!(preload_like_link_loads_stylesheet(
|
|
"preload",
|
|
Some("style"),
|
|
"/app.css"
|
|
));
|
|
assert!(!preload_like_link_loads_stylesheet(
|
|
"preload",
|
|
Some("script"),
|
|
"/entry.js"
|
|
));
|
|
assert!(!preload_like_link_loads_stylesheet(
|
|
"modulepreload",
|
|
Some("script"),
|
|
"/entry.js"
|
|
));
|
|
assert!(preload_like_link_loads_stylesheet(
|
|
"modulepreload",
|
|
Some("style"),
|
|
"/theme.css"
|
|
));
|
|
assert!(preload_like_link_loads_stylesheet(
|
|
"modulepreload",
|
|
None,
|
|
"/theme.css?hash=1"
|
|
));
|
|
assert!(!preload_like_link_loads_stylesheet(
|
|
"modulepreload",
|
|
None,
|
|
"/entry.js"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn connected_preload_like_link_url_accepts_prefetch() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
url::Url::parse("https://example.com/path/page.html").unwrap(),
|
|
"<!doctype html><html><head><link rel=\"prefetch\" href=\"../next.html\"></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let head = document.document_head_handle().expect("head handle");
|
|
let link = document
|
|
.child_nodes(head)
|
|
.expect("head children")
|
|
.into_iter()
|
|
.find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("link"))
|
|
})
|
|
.expect("link handle");
|
|
|
|
assert_eq!(
|
|
connected_preload_like_link_url(&document, moli_dom::NodeId::new(link.index())),
|
|
Some(url::Url::parse("https://example.com/next.html").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn connected_preload_like_link_url_accepts_compression_dictionary() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
url::Url::parse("https://example.com/path/page.html").unwrap(),
|
|
"<!doctype html><html><head><link rel=\"compression-dictionary\" href=\"../dict.bin\"></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let head = document.document_head_handle().expect("head handle");
|
|
let link = document
|
|
.child_nodes(head)
|
|
.expect("head children")
|
|
.into_iter()
|
|
.find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("link"))
|
|
})
|
|
.expect("link handle");
|
|
|
|
assert_eq!(
|
|
connected_preload_like_link_url(&document, moli_dom::NodeId::new(link.index())),
|
|
Some(url::Url::parse("https://example.com/dict.bin").unwrap())
|
|
);
|
|
}
|
|
}
|