Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896//! CSS resource loading: collect stylesheets from `<link>` and `<style>` elements.//!//! After HTML parsing, this module scans the DOM for stylesheet references,//! fetches external CSS resources, resolves `@import` rules, and merges//! everything into a single `Stylesheet` for style resolution.
use we_css::parser::{ImportRule, Parser, Rule, Stylesheet};use we_dom::{Document, NodeData, NodeId};use we_url::Url;
use crate::loader::{LoadError, Resource, ResourceLoader};
/// Maximum depth for `@import` resolution to prevent cycles.const MAX_IMPORT_DEPTH: usize = 5;
/// Errors that can occur during CSS loading.#[derive(Debug)]pub enum CssLoadError { /// A resource failed to load. Load(LoadError), /// The fetched resource was not CSS. NotCss { url: String },}
impl std::fmt::Display for CssLoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Load(e) => write!(f, "CSS load error: {e}"), Self::NotCss { url } => write!(f, "resource at {url} is not CSS"), } }}
impl From<LoadError> for CssLoadError { fn from(e: LoadError) -> Self { Self::Load(e) }}
/// Collect all CSS rules from a parsed DOM document.////// Scans the DOM in document order for `<style>` elements and/// `<link rel="stylesheet">` elements. Inline `<style>` content is parsed/// directly; external stylesheets are fetched via the `ResourceLoader`.////// All rules are merged into a single `Stylesheet` in document order,/// preserving cascade source order. Failed loads are silently skipped/// (graceful degradation).pub fn collect_stylesheets( doc: &Document, loader: &mut ResourceLoader, base_url: &Url,) -> Stylesheet { let mut all_rules: Vec<Rule> = Vec::new(); let mut style_nodes = Vec::new(); collect_style_nodes(doc, doc.root(), &mut style_nodes);
for node in style_nodes { match classify_style_node(doc, node) { StyleSource::InlineStyle(css_text) => { let sheet = Parser::parse(&css_text); let resolved = resolve_imports(sheet, loader, base_url, 0); all_rules.extend(resolved.rules); } StyleSource::ExternalLink { href, media } => { if !media_matches(&media) { continue; } match fetch_stylesheet(loader, &href, base_url, 0) { Ok(sheet) => all_rules.extend(sheet.rules), Err(_) => { // Graceful degradation: skip failed stylesheet loads. } } } StyleSource::NotStylesheet => {} } }
Stylesheet { rules: all_rules }}
/// Walk the DOM in document order and collect `<style>` and `<link>` nodes.fn collect_style_nodes(doc: &Document, node: NodeId, result: &mut Vec<NodeId>) { if let NodeData::Element { tag_name, .. } = doc.node_data(node) { let tag = tag_name.as_str(); if tag.eq_ignore_ascii_case("style") || tag.eq_ignore_ascii_case("link") { result.push(node); } } for child in doc.children(node) { collect_style_nodes(doc, child, result); }}
/// Classification of a DOM node as a style source.enum StyleSource { /// A `<style>` element with inline CSS text. InlineStyle(String), /// A `<link rel="stylesheet">` element with an `href`. ExternalLink { href: String, media: Option<String> }, /// Not a stylesheet source. NotStylesheet,}
/// Classify a DOM node as a style source.fn classify_style_node(doc: &Document, node: NodeId) -> StyleSource { let tag = match doc.tag_name(node) { Some(t) => t, None => return StyleSource::NotStylesheet, };
if tag.eq_ignore_ascii_case("style") { // Check type attribute — only text/css is valid (or omitted, which defaults to text/css) if let Some(type_attr) = doc.get_attribute(node, "type") { if !type_attr.eq_ignore_ascii_case("text/css") { return StyleSource::NotStylesheet; } } // Collect text content from child text nodes let css_text = collect_text_content(doc, node); StyleSource::InlineStyle(css_text) } else if tag.eq_ignore_ascii_case("link") { // Must have rel="stylesheet" let rel = doc.get_attribute(node, "rel").unwrap_or(""); if !rel .split_ascii_whitespace() .any(|r| r.eq_ignore_ascii_case("stylesheet")) { return StyleSource::NotStylesheet; } // Check type attribute if present if let Some(type_attr) = doc.get_attribute(node, "type") { if !type_attr.eq_ignore_ascii_case("text/css") { return StyleSource::NotStylesheet; } } // Must have href match doc.get_attribute(node, "href") { Some(href) if !href.is_empty() => { let media = doc.get_attribute(node, "media").map(|m| m.to_string()); StyleSource::ExternalLink { href: href.to_string(), media, } } _ => StyleSource::NotStylesheet, } } else { StyleSource::NotStylesheet }}
/// Collect concatenated text content from child text nodes of an element.fn collect_text_content(doc: &Document, node: NodeId) -> String { let mut text = String::new(); for child in doc.children(node) { if let Some(data) = doc.text_content(child) { text.push_str(data); } } text}
/// Check if a `media` attribute value matches the `screen` environment.////// Per spec, if no media attribute is present (None), it defaults to `all`./// We support basic media types: `all`, `screen`.fn media_matches(media: &Option<String>) -> bool { match media { None => true, // default is "all" Some(m) => { let m = m.trim(); if m.is_empty() { return true; } // Split comma-separated media types and check if any matches m.split(',').any(|mt| { let mt = mt.trim(); mt.eq_ignore_ascii_case("all") || mt.eq_ignore_ascii_case("screen") }) } }}
/// Fetch an external stylesheet and resolve its `@import` rules.fn fetch_stylesheet( loader: &mut ResourceLoader, href: &str, base_url: &Url, depth: usize,) -> Result<Stylesheet, CssLoadError> { let resource = loader.fetch_url(href, Some(base_url))?;
let (css_text, resolved_url) = match resource { Resource::Css { text, url } => (text, url), // Some servers may return text/plain or text/html for CSS files. // Accept any text response gracefully. Resource::Html { text, base_url, .. } => (text, base_url), Resource::Other { data, url, .. } => { // Try to decode as UTF-8 text match String::from_utf8(data) { Ok(text) => (text, url), Err(_) => { return Err(CssLoadError::NotCss { url: href.to_string(), }) } } } Resource::Image { .. } => { return Err(CssLoadError::NotCss { url: href.to_string(), }) } };
let sheet = Parser::parse(&css_text); Ok(resolve_imports(sheet, loader, &resolved_url, depth))}
/// Resolve `@import` rules in a stylesheet by fetching and inlining imported sheets.////// Replaces `Rule::Import` entries with the imported stylesheet's rules./// Respects `MAX_IMPORT_DEPTH` to prevent infinite loops.fn resolve_imports( sheet: Stylesheet, loader: &mut ResourceLoader, base_url: &Url, depth: usize,) -> Stylesheet { if depth >= MAX_IMPORT_DEPTH { // Strip import rules at max depth to prevent cycles return Stylesheet { rules: sheet .rules .into_iter() .filter(|r| !matches!(r, Rule::Import(_))) .collect(), }; }
let mut resolved_rules: Vec<Rule> = Vec::new();
for rule in sheet.rules { match rule { Rule::Import(ImportRule { ref url }) => { match fetch_stylesheet(loader, url, base_url, depth + 1) { Ok(imported) => resolved_rules.extend(imported.rules), Err(_) => { // Graceful degradation: skip failed imports. } } } other => resolved_rules.push(other), } }
Stylesheet { rules: resolved_rules, }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*; use we_css::parser::StyleRule;
// ----------------------------------------------------------------------- // Helper: build a DOM manually for testing // -----------------------------------------------------------------------
fn make_doc_with_style(css: &str) -> Document { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
let style = doc.create_element("style"); doc.append_child(head, style);
let text = doc.create_text(css); doc.append_child(style, text);
let body = doc.create_element("body"); doc.append_child(html, body);
doc }
fn make_doc_with_link(href: &str) -> Document { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", href); doc.append_child(head, link);
let body = doc.create_element("body"); doc.append_child(html, body);
doc }
// ----------------------------------------------------------------------- // collect_style_nodes // -----------------------------------------------------------------------
#[test] fn collects_style_elements() { let doc = make_doc_with_style("body { color: red; }"); let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); assert_eq!(nodes.len(), 1); assert_eq!(doc.tag_name(nodes[0]), Some("style")); }
#[test] fn collects_link_elements() { let doc = make_doc_with_link("style.css"); let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); assert_eq!(nodes.len(), 1); assert_eq!(doc.tag_name(nodes[0]), Some("link")); }
#[test] fn collects_both_style_and_link() { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "a.css"); doc.append_child(head, link);
let style = doc.create_element("style"); doc.append_child(head, style); let text = doc.create_text("p { margin: 0; }"); doc.append_child(style, text);
let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); assert_eq!(nodes.len(), 2); // Document order: link first, then style assert_eq!(doc.tag_name(nodes[0]), Some("link")); assert_eq!(doc.tag_name(nodes[1]), Some("style")); }
#[test] fn ignores_non_style_elements() { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let body = doc.create_element("body"); doc.append_child(html, body);
let p = doc.create_element("p"); doc.append_child(body, p);
let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); assert!(nodes.is_empty()); }
// ----------------------------------------------------------------------- // classify_style_node // -----------------------------------------------------------------------
#[test] fn classify_style_element() { let doc = make_doc_with_style("body { color: red; }"); let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); match classify_style_node(&doc, nodes[0]) { StyleSource::InlineStyle(text) => { assert_eq!(text, "body { color: red; }"); } _ => panic!("expected InlineStyle"), } }
#[test] fn classify_link_stylesheet() { let doc = make_doc_with_link("style.css"); let mut nodes = Vec::new(); collect_style_nodes(&doc, doc.root(), &mut nodes); match classify_style_node(&doc, nodes[0]) { StyleSource::ExternalLink { href, media } => { assert_eq!(href, "style.css"); assert!(media.is_none()); } _ => panic!("expected ExternalLink"), } }
#[test] fn classify_link_with_media() { let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "screen.css"); doc.set_attribute(link, "media", "screen"); doc.append_child(root, link);
match classify_style_node(&doc, link) { StyleSource::ExternalLink { href, media } => { assert_eq!(href, "screen.css"); assert_eq!(media.as_deref(), Some("screen")); } _ => panic!("expected ExternalLink"), } }
#[test] fn classify_link_without_rel_stylesheet() { let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "icon"); doc.set_attribute(link, "href", "favicon.ico"); doc.append_child(root, link);
assert!(matches!( classify_style_node(&doc, link), StyleSource::NotStylesheet )); }
#[test] fn classify_link_without_href() { let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.append_child(root, link);
assert!(matches!( classify_style_node(&doc, link), StyleSource::NotStylesheet )); }
#[test] fn classify_style_wrong_type() { let mut doc = Document::new(); let root = doc.root();
let style = doc.create_element("style"); doc.set_attribute(style, "type", "text/javascript"); doc.append_child(root, style); let text = doc.create_text("not css"); doc.append_child(style, text);
assert!(matches!( classify_style_node(&doc, style), StyleSource::NotStylesheet )); }
#[test] fn classify_style_with_type_text_css() { let mut doc = Document::new(); let root = doc.root();
let style = doc.create_element("style"); doc.set_attribute(style, "type", "text/css"); doc.append_child(root, style); let text = doc.create_text("p { color: blue; }"); doc.append_child(style, text);
match classify_style_node(&doc, style) { StyleSource::InlineStyle(css) => assert_eq!(css, "p { color: blue; }"), _ => panic!("expected InlineStyle"), } }
#[test] fn classify_link_wrong_type() { let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "style.css"); doc.set_attribute(link, "type", "text/plain"); doc.append_child(root, link);
assert!(matches!( classify_style_node(&doc, link), StyleSource::NotStylesheet )); }
// ----------------------------------------------------------------------- // collect_text_content // -----------------------------------------------------------------------
#[test] fn collect_text_single_child() { let mut doc = Document::new(); let root = doc.root();
let style = doc.create_element("style"); doc.append_child(root, style); let text = doc.create_text("body {}"); doc.append_child(style, text);
assert_eq!(collect_text_content(&doc, style), "body {}"); }
#[test] fn collect_text_multiple_children() { let mut doc = Document::new(); let root = doc.root();
let style = doc.create_element("style"); doc.append_child(root, style); let t1 = doc.create_text("body { "); let t2 = doc.create_text("color: red; }"); doc.append_child(style, t1); doc.append_child(style, t2);
assert_eq!(collect_text_content(&doc, style), "body { color: red; }"); }
#[test] fn collect_text_empty_element() { let mut doc = Document::new(); let root = doc.root();
let style = doc.create_element("style"); doc.append_child(root, style);
assert_eq!(collect_text_content(&doc, style), ""); }
// ----------------------------------------------------------------------- // media_matches // -----------------------------------------------------------------------
#[test] fn media_none_matches() { assert!(media_matches(&None)); }
#[test] fn media_empty_matches() { assert!(media_matches(&Some(String::new()))); }
#[test] fn media_all_matches() { assert!(media_matches(&Some("all".to_string()))); }
#[test] fn media_screen_matches() { assert!(media_matches(&Some("screen".to_string()))); }
#[test] fn media_print_does_not_match() { assert!(!media_matches(&Some("print".to_string()))); }
#[test] fn media_comma_list_with_screen() { assert!(media_matches(&Some("print, screen".to_string()))); }
#[test] fn media_comma_list_without_screen() { assert!(!media_matches(&Some("print, handheld".to_string()))); }
#[test] fn media_case_insensitive() { assert!(media_matches(&Some("SCREEN".to_string()))); assert!(media_matches(&Some("All".to_string()))); }
// ----------------------------------------------------------------------- // collect_stylesheets with inline <style> // -----------------------------------------------------------------------
#[test] fn collect_inline_style_rules() { let doc = make_doc_with_style("p { color: red; } div { margin: 0; }"); let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); assert_eq!(sheet.rules.len(), 2); // Both should be style rules assert!(matches!(sheet.rules[0], Rule::Style(_))); assert!(matches!(sheet.rules[1], Rule::Style(_))); }
#[test] fn collect_empty_style_element() { let doc = make_doc_with_style(""); let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); assert!(sheet.rules.is_empty()); }
#[test] fn collect_multiple_style_elements() { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
// First <style> let style1 = doc.create_element("style"); doc.append_child(head, style1); let t1 = doc.create_text("p { color: red; }"); doc.append_child(style1, t1);
// Second <style> let style2 = doc.create_element("style"); doc.append_child(head, style2); let t2 = doc.create_text("div { color: blue; }"); doc.append_child(style2, t2);
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); assert_eq!(sheet.rules.len(), 2); }
#[test] fn collect_link_graceful_failure() { // External link will fail to load (no real server), but should not crash let doc = make_doc_with_link("http://nonexistent.test/style.css"); let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); // Should return empty stylesheet (graceful degradation) assert!(sheet.rules.is_empty()); }
#[test] fn link_with_print_media_skipped() { let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "print.css"); doc.set_attribute(link, "media", "print"); doc.append_child(head, link);
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); // Print media link should be skipped entirely assert!(sheet.rules.is_empty()); }
// ----------------------------------------------------------------------- // resolve_imports // -----------------------------------------------------------------------
#[test] fn resolve_imports_strips_at_max_depth() { let sheet = Stylesheet { rules: vec![ Rule::Import(ImportRule { url: "deep.css".to_string(), }), Rule::Style(StyleRule { selectors: we_css::parser::SelectorList { selectors: Vec::new(), }, declarations: Vec::new(), }), ], };
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let resolved = resolve_imports(sheet, &mut loader, &base, MAX_IMPORT_DEPTH); // Import should be stripped, style rule kept assert_eq!(resolved.rules.len(), 1); assert!(matches!(resolved.rules[0], Rule::Style(_))); }
#[test] fn resolve_imports_failed_import_skipped() { let sheet = Stylesheet { rules: vec![ Rule::Import(ImportRule { url: "http://nonexistent.test/import.css".to_string(), }), Rule::Style(StyleRule { selectors: we_css::parser::SelectorList { selectors: Vec::new(), }, declarations: Vec::new(), }), ], };
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let resolved = resolve_imports(sheet, &mut loader, &base, 0); // Failed import should be skipped, style rule kept assert_eq!(resolved.rules.len(), 1); assert!(matches!(resolved.rules[0], Rule::Style(_))); }
// ----------------------------------------------------------------------- // CssLoadError display // -----------------------------------------------------------------------
#[test] fn css_load_error_display_not_css() { let e = CssLoadError::NotCss { url: "test.png".to_string(), }; assert_eq!(e.to_string(), "resource at test.png is not CSS"); }
#[test] fn css_load_error_display_load() { let e = CssLoadError::Load(LoadError::InvalidUrl("bad".to_string())); assert!(e.to_string().contains("CSS load error")); }
// ----------------------------------------------------------------------- // Integration: style + link document order // -----------------------------------------------------------------------
#[test] fn style_and_link_document_order() { // When both <style> and <link> are present, inline styles should be // collected in document order (link will fail but style should work) let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let head = doc.create_element("head"); doc.append_child(html, head);
// Link first (will fail) let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "http://nonexistent.test/first.css"); doc.append_child(head, link);
// Style second let style = doc.create_element("style"); doc.append_child(head, style); let text = doc.create_text("p { color: green; }"); doc.append_child(style, text);
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); // Only the inline style should succeed assert_eq!(sheet.rules.len(), 1); }
// ----------------------------------------------------------------------- // Edge cases // -----------------------------------------------------------------------
#[test] fn link_with_empty_href_skipped() { let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", ""); doc.append_child(root, link);
assert!(matches!( classify_style_node(&doc, link), StyleSource::NotStylesheet )); }
#[test] fn style_in_body_is_collected() { // <style> in <body> should also be collected (browsers allow this) let mut doc = Document::new(); let root = doc.root();
let html = doc.create_element("html"); doc.append_child(root, html);
let body = doc.create_element("body"); doc.append_child(html, body);
let style = doc.create_element("style"); doc.append_child(body, style); let text = doc.create_text("h1 { font-size: 2em; }"); doc.append_child(style, text);
let mut loader = ResourceLoader::new(); let base = Url::parse("http://example.com/").unwrap();
let sheet = collect_stylesheets(&doc, &mut loader, &base); assert_eq!(sheet.rules.len(), 1); }
#[test] fn link_rel_case_insensitive() { // rel="Stylesheet" should work too (case-insensitive matching) let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "Stylesheet"); doc.set_attribute(link, "href", "style.css"); doc.append_child(root, link);
match classify_style_node(&doc, link) { StyleSource::ExternalLink { href, .. } => assert_eq!(href, "style.css"), _ => panic!("expected ExternalLink"), } }
#[test] fn link_rel_with_extra_values() { // rel="alternate stylesheet" should NOT match as a regular stylesheet // (alternate stylesheets are disabled by default) // Actually, per spec, "stylesheet" in the token list means it IS a stylesheet // But "alternate stylesheet" is a disabled stylesheet. For simplicity, // we treat any rel containing "stylesheet" as a stylesheet. let mut doc = Document::new(); let root = doc.root();
let link = doc.create_element("link"); doc.set_attribute(link, "rel", "stylesheet"); doc.set_attribute(link, "href", "style.css"); doc.append_child(root, link);
assert!(matches!( classify_style_node(&doc, link), StyleSource::ExternalLink { .. } )); }}