1use std::collections::{BTreeMap, HashMap, HashSet};
10
11use cssparser::ToCss;
12use selectors::context::SelectorCaches;
13use serde::Serialize;
14
15use crate::content::{Book, NodeId};
16use crate::pages::PageBox;
17
18use super::element::ElementTree;
19use super::properties::Declaration;
20use super::sheet::{
21 self, Importance, MarginDeclaration, MarginRule, PageRule, SheetPosition, Written,
22 margin_longhands,
23};
24use super::{
25 MarginBox, PageQuery, Situation, StyleTree, Stylesheets, align_hint, applicable, level, selects,
26};
27use crate::pages::Side;
28
29mod css;
30
31#[derive(Debug, Clone, PartialEq, Serialize)]
33pub struct Inspection {
34 pub node: Option<NodeId>,
38 pub element: String,
42 #[serde(rename = "elementNode")]
46 pub element_node: Option<NodeId>,
47 #[serde(rename = "pseudoElement", skip_serializing_if = "Option::is_none")]
50 pub pseudo_element: Option<String>,
51 pub id: Option<String>,
53 pub classes: Vec<String>,
55 pub ancestors: Vec<Ancestor>,
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub page: Option<String>,
61 pub rules: Vec<MatchedRule>,
64 pub computed: BTreeMap<String, String>,
67 pub boxes: Vec<PageBox>,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize)]
73pub struct Ancestor {
74 pub node: Option<NodeId>,
77 pub element: String,
79 pub id: Option<String>,
81 pub classes: Vec<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct MatchedRule {
88 pub sheet: String,
90 pub line: u32,
92 pub column: u32,
94 pub selector: String,
97 pub specificity: [u32; 3],
101 pub declarations: Vec<InspectedDeclaration>,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize)]
107pub struct InspectedDeclaration {
108 pub property: String,
110 pub value: String,
112 pub important: bool,
114 pub applied: bool,
117}
118
119impl Stylesheets {
120 pub fn inspect(&self, book: &Book, tree: &StyleTree, node: NodeId) -> Option<Inspection> {
132 let pseudo = node.pseudo_element();
133 if let Some((element, which)) = pseudo {
134 tree.pseudo_element(element, which)?;
135 }
136 let which = pseudo.map(|(_, which)| which);
137 book.subtree(node.element())?;
138 let elements = ElementTree::build(book);
139 let index = owner(&elements, book, node.element())?;
140 let nodes = elements.nodes();
141 let element = &nodes[index];
142
143 let mut caches = SelectorCaches::default();
144 let matched = applicable(&self.sheets, &elements, index, &mut caches, which.as_ref());
145 let declaration = |(sheet, rule, order): (usize, usize, usize)| {
146 &self.sheets[sheet].rules[rule].declarations[order].0
147 };
148
149 let mut hint = element.align.filter(|_| which.is_none()).map(align_hint);
152 let mut winners: HashMap<String, Option<(usize, usize, usize)>> = HashMap::new();
153 for (level, _, sheet, rule, order) in &matched {
154 if *level > 0
155 && let Some(hint) = hint.take()
156 {
157 winners.insert(hint.property().to_string(), None);
158 }
159 let at = (*sheet, *rule, *order);
160 for property in longhands(declaration(at)) {
161 winners.insert(property, Some(at));
162 }
163 }
164 if let Some(hint) = hint {
165 winners.insert(hint.property().to_string(), None);
166 }
167 let won: HashSet<(usize, usize, usize)> = winners.into_values().flatten().collect();
168
169 let mut order: Vec<(usize, usize, u32)> = Vec::new();
170 for (_, specificity, sheet, rule, _) in &matched {
171 if !order.iter().any(|(s, r, _)| (*s, *r) == (*sheet, *rule)) {
172 order.push((*sheet, *rule, *specificity));
173 }
174 }
175 let rules = order
176 .into_iter()
177 .map(|(sheet, index, specificity)| {
178 let rule = &self.sheets[sheet].rules[index];
179 matched_rule(
180 &rule.position,
181 rule.selectors.to_css_string(),
182 [
183 specificity >> 20,
184 (specificity >> 10) & 0x3ff,
185 specificity & 0x3ff,
186 ],
187 &rule.written,
188 |at| won.contains(&(sheet, index, at)),
189 )
190 })
191 .collect();
192
193 let mut ancestors = Vec::new();
194 let mut parent = element.parent;
195 while let Some(at) = parent {
196 let above = &nodes[at];
197 ancestors.push(Ancestor {
198 node: (above.id != NodeId::UNASSIGNED).then_some(above.id),
199 element: above.name.to_string(),
200 id: above.attributes.id.clone(),
201 classes: above.attributes.classes.clone(),
202 });
203 parent = above.parent;
204 }
205 ancestors.reverse();
206
207 let styled = if which.is_some() { node } else { element.id };
208 let computed = css::computed(tree.style(styled));
209 Some(Inspection {
210 node: Some(styled),
211 element: element.name.to_string(),
212 element_node: (element.id != NodeId::UNASSIGNED).then_some(element.id),
213 pseudo_element: which.map(|which| which.to_css_string()),
214 id: element.attributes.id.clone(),
215 classes: element.attributes.classes.clone(),
216 ancestors,
217 page: None,
218 rules,
219 computed,
220 boxes: Vec::new(),
221 })
222 }
223
224 pub fn inspect_margin_box(
230 &self,
231 tree: &StyleTree,
232 query: PageQuery<'_>,
233 which: MarginBox,
234 ) -> Option<Inspection> {
235 let query = match tree.master(query.name, query.situation) {
237 Some(_) => query,
238 None => PageQuery {
239 name: None,
240 ..query
241 },
242 };
243 let style = tree.page(query).boxes.iter().find(|b| b.which == which)?;
244
245 let mut matching: Vec<(u8, &PageRule)> = self
246 .sheets
247 .iter()
248 .flat_map(|sheet| {
249 let level = level(sheet.origin, Importance::Normal);
250 sheet.pages.iter().map(move |rule| (level, rule))
251 })
252 .filter(|(_, rule)| selects(rule, query))
253 .collect();
254 matching.sort_by_key(|(level, rule)| (*level, rule.specificity()));
255
256 fn boxes(rule: &PageRule, which: MarginBox) -> impl Iterator<Item = (usize, &MarginRule)> {
257 rule.boxes
258 .iter()
259 .enumerate()
260 .filter(move |(_, margin)| margin.which == which)
261 }
262 let mut winners: HashMap<String, (usize, usize, usize)> = HashMap::new();
263 for (index, (_, rule)) in matching.iter().enumerate() {
264 for (at, margin) in boxes(rule, which) {
265 for (order, declaration) in margin.declarations.iter().enumerate() {
266 let declarations = match declaration {
267 MarginDeclaration::Pending(pending) => margin_longhands(&pending.property),
268 declaration => vec![declaration.clone()],
269 };
270 for declaration in declarations {
271 let property = match declaration {
272 MarginDeclaration::Style(declaration) => {
273 declaration.property().to_string()
274 }
275 _ => "content".to_string(),
276 };
277 winners.insert(property, (index, at, order));
278 }
279 }
280 }
281 }
282 let won: HashSet<(usize, usize, usize)> = winners.into_values().collect();
283
284 let mut rules = Vec::new();
285 for (index, (_, rule)) in matching.iter().enumerate() {
286 let (name, first, side) = rule.specificity();
287 for (at, margin) in boxes(rule, which) {
288 rules.push(matched_rule(
289 &rule.position,
290 rule.selector.clone(),
291 [name as u32, first as u32, side as u32],
292 &margin.written,
293 |order| won.contains(&(index, at, order)),
294 ));
295 }
296 }
297
298 let mut computed = css::computed(&style.style);
299 computed.insert("content".into(), css::content(&style.content));
300 Some(Inspection {
301 node: None,
302 element: format!("@{}", which.keyword()),
303 element_node: None,
304 pseudo_element: None,
305 id: None,
306 classes: Vec::new(),
307 ancestors: Vec::new(),
308 page: Some(page_selector(query)),
309 rules,
310 computed,
311 boxes: Vec::new(),
312 })
313 }
314}
315
316fn longhands(declaration: &Declaration) -> Vec<String> {
319 match declaration {
320 Declaration::Pending(pending) => sheet::longhands(&pending.property)
321 .iter()
322 .map(|longhand| longhand.property().to_string())
323 .collect(),
324 declaration => vec![declaration.property().to_string()],
325 }
326}
327
328fn matched_rule(
330 position: &SheetPosition,
331 selector: String,
332 specificity: [u32; 3],
333 written: &[Written],
334 won: impl Fn(usize) -> bool,
335) -> MatchedRule {
336 MatchedRule {
337 sheet: position.sheet.clone(),
338 line: position.line,
339 column: position.column,
340 selector,
341 specificity,
342 declarations: written
343 .iter()
344 .map(|written| InspectedDeclaration {
345 property: written.property.clone(),
346 value: written.value.clone(),
347 important: written.important,
348 applied: written.longhands.clone().any(&won),
349 })
350 .collect(),
351 }
352}
353
354pub(crate) fn element_of(book: &Book, node: NodeId) -> Option<NodeId> {
357 let node = node.element();
358 book.subtree(node)?;
359 let elements = ElementTree::build(book);
360 owner(&elements, book, node).map(|index| elements.nodes()[index].id)
361}
362
363fn owner(elements: &ElementTree, book: &Book, node: NodeId) -> Option<usize> {
366 let nodes = elements.nodes();
367 if let Some(index) = nodes.iter().position(|element| element.id == node) {
368 return Some(index);
369 }
370 let mut at = nodes
371 .iter()
372 .rposition(|element| element.id != NodeId::UNASSIGNED && element.id < node)?;
373 loop {
374 let element = &nodes[at];
375 if book
376 .subtree(element.id)
377 .is_some_and(|held| held.contains(&node.get()))
378 {
379 return Some(at);
380 }
381 at = element.parent?;
382 }
383}
384
385fn page_selector(query: PageQuery<'_>) -> String {
387 let (opening, side) = match query.situation {
388 Situation::First(side) => (":first", side),
389 Situation::Body(side) => ("", side),
390 Situation::Blank => (":blank", Side::Verso),
391 };
392 let side = match side {
393 Side::Recto => ":right",
394 Side::Verso => ":left",
395 };
396 format!("@page {}{opening}{side}", query.name.unwrap_or_default())
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::fonts::{FontRegistry, bundled_registry};
403 use crate::style::{Source, sheet::PROPERTIES};
404
405 fn registry() -> &'static FontRegistry {
406 static REGISTRY: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
407 REGISTRY.get_or_init(|| bundled_registry().expect("bundled font parses"))
408 }
409
410 fn book() -> Book {
413 let json = r#"{"metadata": {}, "sections": [{"blocks": [
414 {"type": "heading", "level": 1, "inlines": [{"type": "text", "value": "One"}]},
415 {"type": "blockquote", "attributes": {"classes": ["epigraph"]}, "blocks": [
416 {"type": "paragraph",
417 "attributes": {"id": "motto", "classes": ["quiet", "small"]},
418 "inlines": [{"type": "emphasis", "children": [{"type": "text", "value": "Lo"}]}]}
419 ]}
420 ]}]}"#;
421 let mut book: Book = serde_json::from_str(json).expect("the book reads");
422 book.assign_node_ids();
423 book
424 }
425
426 const CSS: &str = "p { color: red }
427.quiet { color: blue; margin: 1em 2em }
428#motto { color: green; margin-left: 0 }
429blockquote p { font-size: 9pt }";
430
431 fn compiled(book: &Book, css: &str) -> (Stylesheets, StyleTree) {
433 let sheets = Stylesheets::parse(&[Source::author("author.css", css)]);
434 let tree = sheets.compile(book, registry());
435 (sheets, tree)
436 }
437
438 fn element(book: &Book, name: &str) -> NodeId {
439 ElementTree::build(book)
440 .nodes()
441 .iter()
442 .find(|element| element.name == name)
443 .unwrap_or_else(|| panic!("no {name}"))
444 .id
445 }
446
447 fn author_rules(inspection: &Inspection) -> Vec<&MatchedRule> {
448 inspection
449 .rules
450 .iter()
451 .filter(|rule| rule.sheet == "author.css")
452 .collect()
453 }
454
455 #[test]
456 fn an_element_answers_with_its_name_id_classes_and_ancestors() {
457 let book = book();
458 let (sheets, tree) = compiled(&book, CSS);
459 let paragraph = element(&book, "p");
460 let inspection = sheets
461 .inspect(&book, &tree, paragraph)
462 .expect("a paragraph");
463
464 assert_eq!(inspection.node, Some(paragraph));
465 assert_eq!(inspection.element, "p");
466 assert_eq!(inspection.element_node, Some(paragraph));
467 assert_eq!(inspection.id.as_deref(), Some("motto"));
468 assert_eq!(inspection.classes, ["quiet", "small"]);
469 let names: Vec<&str> = inspection
470 .ancestors
471 .iter()
472 .map(|ancestor| ancestor.element.as_str())
473 .collect();
474 assert_eq!(names, ["book", "section", "blockquote"]);
475 assert_eq!(inspection.ancestors[0].node, None);
476 assert_eq!(inspection.ancestors[1].node, Some(book.sections[0].id));
477 assert_eq!(inspection.ancestors[2].classes, ["epigraph"]);
478
479 let emphasis = element(&book, "em");
480 let text = NodeId::new(emphasis.get() + 1);
481 let held = sheets.inspect(&book, &tree, text).expect("a text node");
482 assert_eq!(held.node, Some(emphasis), "text answers for its element");
483 assert_eq!(held.element, "em");
484 assert_eq!(held.element_node, Some(emphasis));
485 }
486
487 #[test]
488 fn matched_rules_come_in_cascade_order_with_where_they_were_written() {
489 let book = book();
490 let (sheets, tree) = compiled(&book, CSS);
491 let inspection = sheets
492 .inspect(&book, &tree, element(&book, "p"))
493 .expect("a paragraph");
494
495 let written: Vec<(&str, u32, u32, [u32; 3])> = author_rules(&inspection)
496 .iter()
497 .map(|rule| {
498 (
499 rule.selector.as_str(),
500 rule.line,
501 rule.column,
502 rule.specificity,
503 )
504 })
505 .collect();
506 assert_eq!(
507 written,
508 [
509 ("p", 1, 1, [0, 0, 1]),
510 ("blockquote p", 4, 1, [0, 0, 2]),
511 (".quiet", 2, 1, [0, 1, 0]),
512 ("#motto", 3, 1, [1, 0, 0]),
513 ]
514 );
515 let last_built_in = inspection
516 .rules
517 .iter()
518 .rposition(|rule| rule.sheet == "user-agent.css");
519 let first_author = inspection
520 .rules
521 .iter()
522 .position(|rule| rule.sheet == "author.css");
523 assert!(
524 last_built_in < first_author,
525 "the built-in sheet cascades under the author's"
526 );
527 }
528
529 #[test]
530 fn each_declaration_says_whether_it_won_the_cascade() {
531 let book = book();
532 let (sheets, tree) = compiled(&book, CSS);
533 let inspection = sheets
534 .inspect(&book, &tree, element(&book, "p"))
535 .expect("a paragraph");
536
537 let applied: Vec<(&str, &str, bool)> = author_rules(&inspection)
538 .iter()
539 .flat_map(|rule| {
540 rule.declarations.iter().map(|declaration| {
541 (
542 rule.selector.as_str(),
543 declaration.property.as_str(),
544 declaration.applied,
545 )
546 })
547 })
548 .collect();
549 assert_eq!(
550 applied,
551 [
552 ("p", "color", false),
553 ("blockquote p", "font-size", true),
554 (".quiet", "color", false),
555 (".quiet", "margin", true),
556 ("#motto", "color", true),
557 ("#motto", "margin-left", true),
558 ]
559 );
560 assert!(
561 inspection
562 .rules
563 .iter()
564 .filter(|rule| rule.sheet == "user-agent.css")
565 .flat_map(|rule| &rule.declarations)
566 .filter(|declaration| declaration.property.starts_with("margin"))
567 .all(|declaration| !declaration.applied),
568 "the author's margins override every built-in one"
569 );
570 }
571
572 #[test]
573 fn every_property_in_the_subset_has_a_computed_value() {
574 let book = book();
575 let (sheets, tree) = compiled(&book, CSS);
576 let inspection = sheets
577 .inspect(&book, &tree, element(&book, "p"))
578 .expect("a paragraph");
579
580 for spec in PROPERTIES {
581 let value = inspection
582 .computed
583 .get(spec.name)
584 .unwrap_or_else(|| panic!("no computed {}", spec.name));
585 if spec.name != "border" {
586 assert!(!value.is_empty(), "{} computed to nothing", spec.name);
587 }
588 }
589 assert_eq!(inspection.computed.len(), PROPERTIES.len());
590 assert_eq!(inspection.computed["color"], "#008000");
591 assert_eq!(inspection.computed["font-size"], "9pt");
592 assert_eq!(inspection.computed["margin-top"], "9pt");
593 assert_eq!(inspection.computed["margin-right"], "18pt");
594 assert_eq!(inspection.computed["margin-left"], "0pt");
595 assert_eq!(inspection.computed["margin"], "9pt 18pt 9pt 0pt");
596 }
597
598 #[test]
599 fn a_margin_box_answers_with_its_page_and_the_rules_that_set_it() {
600 let book = book();
601 let css = "@page :left { @top-left { content: \"Left\"; color: red } }
602@page { @top-left { color: blue } }";
603 let (sheets, tree) = compiled(&book, css);
604 let left = PageQuery {
605 name: None,
606 situation: Situation::Body(Side::Verso),
607 };
608 let inspection = sheets
609 .inspect_margin_box(&tree, left, MarginBox::TopLeft)
610 .expect("the left page names @top-left");
611
612 assert_eq!(inspection.element, "@top-left");
613 assert_eq!(inspection.page.as_deref(), Some("@page :left"));
614 assert_eq!(inspection.node, None);
615 assert_eq!(inspection.element_node, None);
616 type Seen<'a> = (&'a str, u32, [u32; 3], Vec<(&'a str, &'a str, bool)>);
617 let rules: Vec<Seen<'_>> = author_rules(&inspection)
618 .iter()
619 .map(|rule| {
620 (
621 rule.selector.as_str(),
622 rule.line,
623 rule.specificity,
624 rule.declarations
625 .iter()
626 .map(|d| (d.property.as_str(), d.value.as_str(), d.applied))
627 .collect(),
628 )
629 })
630 .collect();
631 assert_eq!(
632 rules,
633 [
634 ("@page", 2, [0, 0, 0], vec![("color", "blue", false)]),
635 (
636 "@page :left",
637 1,
638 [0, 0, 1],
639 vec![("content", "\"Left\"", true), ("color", "red", true)]
640 ),
641 ]
642 );
643 assert_eq!(inspection.computed["content"], "\"Left\"");
644 assert_eq!(inspection.computed["color"], "#ff0000");
645
646 let right = PageQuery {
647 name: None,
648 situation: Situation::Body(Side::Recto),
649 };
650 let inspection = sheets
651 .inspect_margin_box(&tree, right, MarginBox::TopLeft)
652 .expect("every page names @top-left");
653 assert_eq!(inspection.page.as_deref(), Some("@page :right"));
654 assert_eq!(author_rules(&inspection).len(), 1);
655 assert_eq!(inspection.computed["color"], "#0000ff");
656 }
657
658 #[test]
659 fn a_synthesized_or_unknown_node_answers_nothing() {
660 let book = book();
661 let (sheets, tree) = compiled(&book, CSS);
662 assert_eq!(sheets.inspect(&book, &tree, NodeId::UNASSIGNED), None);
663 assert_eq!(sheets.inspect(&book, &tree, NodeId::new(u32::MAX)), None);
664 let page = PageQuery {
665 name: None,
666 situation: Situation::Body(Side::Recto),
667 };
668 assert_eq!(
669 sheets.inspect_margin_box(&tree, page, MarginBox::RightTop),
670 None
671 );
672 }
673}