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

285 lines
10 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_is_quirks_mode(&self) -> bool {
self.node(self.document_node_id())
.and_then(moli_dom::native::Node::as_document)
.is_some_and(|document| document.is_quirks_mode())
}
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"
);
assert!(!disposition.options().quirks_mode_mime_compatibility());
}
#[test]
fn stylesheet_link_captures_quirks_mode_mime_compatibility() {
let parser = HtmlParser::SCRIPTING_ENABLED;
let document = parser.parse(
url::Url::parse("https://example.com/page.html").unwrap(),
"<html><head><link rel=stylesheet href=app.css></head></html>".to_owned(),
);
let link = document
.document_head_handle()
.and_then(|head| document.child_nodes(head))
.and_then(|children| {
children.into_iter().find(|handle| {
document
.node(*handle)
.and_then(Node::as_element)
.is_some_and(|element| element.is_html_element("link"))
})
})
.expect("stylesheet link");
let disposition =
stylesheet_link_disposition(&document, moli_dom::NodeId::new(link.index()))
.expect("stylesheet disposition");
assert!(disposition.options().quirks_mode_mime_compatibility());
}
#[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())
);
}
}