Skip to main content

fleuron/style/
inspect.rs

1//! What the cascade did for one element or one page margin box: the
2//! rules that matched it, which of their declarations won, and what
3//! every property computed to.
4//!
5//! Nothing is kept for this between questions. The cascade runs again
6//! for the one element asked about, over the sheets the tree was
7//! compiled from, so the answer cannot disagree with what layout used.
8
9use 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/// What one element, or one page margin box, answers with.
32#[derive(Debug, Clone, PartialEq, Serialize)]
33pub struct Inspection {
34    /// The node the answer is about: the element or pseudo-element
35    /// asked about, or the element that holds the text node asked
36    /// about. `None` for a page margin box.
37    pub node: Option<NodeId>,
38    /// The element name selectors match, or the margin box's at-rule,
39    /// as `@top-left`. For a pseudo-element, the element it belongs
40    /// to.
41    pub element: String,
42    /// That element, by its id: the same as `node` for an element, and
43    /// the element a pseudo-element belongs to. `None` for a page
44    /// margin box.
45    #[serde(rename = "elementNode")]
46    pub element_node: Option<NodeId>,
47    /// For a pseudo-element, its name as CSS writes it, as
48    /// `::first-letter`.
49    #[serde(rename = "pseudoElement", skip_serializing_if = "Option::is_none")]
50    pub pseudo_element: Option<String>,
51    /// The id a selector reaches the element by.
52    pub id: Option<String>,
53    /// The classes a selector reaches the element by.
54    pub classes: Vec<String>,
55    /// The elements this one sits inside, the book first.
56    pub ancestors: Vec<Ancestor>,
57    /// For a margin box, the page it is on as a page selector writes
58    /// it, as `@page chapter:first:right`.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub page: Option<String>,
61    /// The rules that matched, in cascade order. Where two set the
62    /// same property, the later one wins.
63    pub rules: Vec<MatchedRule>,
64    /// The computed value of every property in the subset, written as
65    /// CSS. Lengths are in points.
66    pub computed: BTreeMap<String, String>,
67    /// The border box on each page the element reaches.
68    pub boxes: Vec<PageBox>,
69}
70
71/// One element an inspected element sits inside.
72#[derive(Debug, Clone, PartialEq, Serialize)]
73pub struct Ancestor {
74    /// Its content node. `None` for the book, and for the `thead` and
75    /// `tbody` of a table, which no content node stands behind.
76    pub node: Option<NodeId>,
77    /// The element name selectors match.
78    pub element: String,
79    /// The id a selector reaches it by.
80    pub id: Option<String>,
81    /// The classes a selector reaches it by.
82    pub classes: Vec<String>,
83}
84
85/// One rule that matched.
86#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct MatchedRule {
88    /// The name the sheet was handed in under.
89    pub sheet: String,
90    /// The line the rule begins on, counting from 1.
91    pub line: u32,
92    /// The column it begins at, counting from 1.
93    pub column: u32,
94    /// The selector as CSS writes it. For a margin box, the `@page`
95    /// prelude.
96    pub selector: String,
97    /// Its specificity, as ids, then classes, then element names. For
98    /// a margin box, as a page name, then `:first` or `:blank`, then
99    /// `:left` or `:right`.
100    pub specificity: [u32; 3],
101    /// Its declarations, in the order they were written.
102    pub declarations: Vec<InspectedDeclaration>,
103}
104
105/// One declaration of a matched rule.
106#[derive(Debug, Clone, PartialEq, Serialize)]
107pub struct InspectedDeclaration {
108    /// The property, in lowercase.
109    pub property: String,
110    /// The value as it was written.
111    pub value: String,
112    /// Whether it was written `!important`.
113    pub important: bool,
114    /// Whether it won the cascade. A shorthand won where any longhand
115    /// it sets did.
116    pub applied: bool,
117}
118
119impl Stylesheets {
120    /// What the cascade did for one node of `book`, whose styling
121    /// `tree` is, compiled from these sheets. A text node answers for
122    /// the element that holds it.
123    ///
124    /// The id of a pseudo-element answers for the pseudo-element: the
125    /// rules that match it and the style it computed to.
126    ///
127    /// `None` for a node the book does not hold, for a pseudo-element
128    /// no rule styles, and for the id the engine writes its own text
129    /// under. The boxes are left empty, since only a laid-out book has
130    /// them.
131    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        // The winner of each longhand, where `None` is the alignment
150        // a table cell's column wrote, which no rule holds.
151        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    /// What the cascade did for one margin box of a page `query`
225    /// describes, whose master `tree` resolved from these sheets.
226    ///
227    /// `None` for a box no rule for that page names. The boxes are left
228    /// empty, since only a laid-out book has them.
229    pub fn inspect_margin_box(
230        &self,
231        tree: &StyleTree,
232        query: PageQuery<'_>,
233        which: MarginBox,
234    ) -> Option<Inspection> {
235        // A named page with no master of its own takes the unnamed one.
236        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
316/// The longhands a declaration sets. One that reads a custom property
317/// sets every longhand of its property, whatever the value comes to.
318fn 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
328/// One matched rule, its declarations marked by whether they won.
329fn 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
354/// The element that stands for one node of `book`: the node itself,
355/// or for a text node, the element that holds it.
356pub(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
363/// The element that stands for `node`: its own, or for a text node,
364/// which is not an element, the innermost element holding it.
365fn 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
385/// The page a query describes, as a page selector writes it.
386fn 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    /// A chapter: a heading, and an epigraph holding one paragraph
411    /// with an id, two classes, and emphasis inside it.
412    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    /// The sheets, and the tree they compile to over `book`.
432    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}