diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs
index 24cc584e..adddee0c 100644
--- a/crates/svg/src/lib.rs
+++ b/crates/svg/src/lib.rs
@@ -392,7 +392,17 @@ enum PaintRef {
Current,
}
-/// A CSS rule: selector (type, class, id; `*`) with its specificity and declarations.
+/// The most selectors (each entry of a comma list counts) the `{rects}"#).into_bytes()
+}
+
+#[test]
+fn comma_selector_lists_share_one_declaration_block() {
+ // Before #2140 every selector of a comma list got its own copy of the declaration block, so
+ // `a,a,a…{…}` stored selectors × declarations strings; one selector past the cap is refused.
+ let sels = vec!["rect"; MAX_CSS_SELECTORS + 1].join(",");
+ let decls: String = (0..10).map(|i| format!("p{i}:v;")).collect();
+ let svg = styled(&format!("{sels}{{fill:red;{decls}}}"), &[""]);
+ let e = parse(&svg).unwrap_err();
+ assert!(e.to_string().contains("selectors"), "{e}");
+}
+
+#[test]
+fn a_style_sheet_with_too_many_declarations_errors() {
+ let decls: String = (0..=MAX_CSS_DECLARATIONS).map(|i| format!("p{i}:v;")).collect();
+ let svg = styled(&format!("rect{{{decls}}}"), &[""]);
+ let e = parse(&svg).unwrap_err();
+ assert!(e.to_string().contains("declarations"), "{e}");
+}
+
+#[test]
+fn selector_lists_keep_the_css_cascade() {
+ let green = Some(Paint::Color([0.0, 1.0, 0.0]));
+ let blue = Some(Paint::Color([0.0, 0.0, 1.0]));
+ let red = Some(Paint::Color([1.0, 0.0, 0.0]));
+ let css = "rect, .b, #c { fill: #00ff00 } .b, .d { fill: #0000ff; stroke: #000000; stroke-width: 3 } rect.e, #c { fill: #ff0000 }";
+ let svg = styled(css, &["", r#"class="b""#, r#"id="c" class="b""#, r#"class="b" style="fill:#00ff00""#, r##"class="e" fill="#0000ff""##, r#"class="d""#]);
+ let doc = parse(&svg).unwrap();
+ let shapes = doc.flatten();
+ let fills: Vec<_> = shapes.iter().map(|s| s.2.fill.clone()).collect();
+ // Type selector; class beats type; id beats class; inline style beats every rule; a later,
+ // more specific rule beats the presentation attribute.
+ assert_eq!(fills, vec![green.clone(), blue.clone(), red.clone(), green, red, blue]);
+ assert!(shapes[0].2.stroke.is_none());
+ // The second list's block reaches both of its selectors.
+ for i in [1, 5] {
+ assert_eq!(shapes[i].2.stroke.as_ref().map(|s| s.width), Some(3.0), "shape {i}");
+ }
+}
+
+#[test]
+fn a_comma_list_stores_its_declarations_once() {
+ let sels = vec!["rect"; 1000].join(",");
+ let decls: String = (0..50).map(|i| format!("p{i}:v;")).collect();
+ let mut sheet = StyleSheet::default();
+ parse_css(&format!("{sels}{{{decls}}}"), &mut sheet).unwrap();
+ assert_eq!(sheet.rules.len(), 1000);
+ assert_eq!(sheet.blocks.len(), 1);
+ assert_eq!(sheet.decls, 50);
+}
+
+#[test]
+fn rules_of_unsupported_selectors_store_no_blocks() {
+ let mut sheet = StyleSheet::default();
+ parse_css(&"a b{}".repeat(100_000), &mut sheet).unwrap();
+ parse_css("a b{fill:red} a>b, rect{fill:blue} c:hover, d{fill:red}", &mut sheet).unwrap();
+ assert_eq!(sheet.rules.len(), 2);
+ assert_eq!(sheet.blocks.len(), 2);
+ assert!(sheet.rules.iter().all(|r| sheet.blocks.get(r.block).is_some_and(|b| b.len() == 1)));
+}
+
+#[test]
+fn huge_class_lists_count_against_the_matching_budget() {
+ // One rule, but a 1 MB class attribute re-split on each of 100 `