Skip to main content

fleuron/
style.rs

1//! The style tree: resolved styling.
2//!
3//! Styling enters as CSS. A built-in user-agent stylesheet supplies
4//! the defaults; author CSS cascades over it. What comes out is one
5//! computed style per content node and one page master per situation
6//! a page can be in, and that is the whole of what layout is told.
7//!
8//! ```text
9//! content tree + CSS ─► style tree ─► box tree ─► …
10//! ```
11//!
12//! Font faces reach the registry here too: `@font-face` names a
13//! source, the host loader hands back bytes, and every node's face is
14//! an id by the time layout sees it.
15
16mod element;
17mod inspect;
18mod properties;
19mod sheet;
20pub mod subset;
21mod substitution;
22
23use std::collections::BTreeMap;
24
25use cssparser::ToCss;
26use selectors::context::{
27    MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode, SelectorCaches,
28};
29use selectors::matching::{MatchingContext, matches_selector};
30use serde::Serialize;
31
32use crate::Warning;
33use crate::content::{
34    Alignment, Attributes, Block, Book, Inline, NodeId, PseudoElement, block_attributes,
35    block_position, inline_attributes, inline_position, origin, rows,
36};
37use crate::fonts::{FaceAttributes, FontError, FontRegistry, FontSource};
38use crate::lines::{FirstLine, InlineBox, InlineStyles, ParagraphStyle};
39use crate::pages::Side;
40
41pub(crate) use inspect::element_of;
42pub use inspect::{Ancestor, InspectedDeclaration, Inspection, MatchedRule};
43pub use properties::{
44    Align, AlignContent, Background, BackgroundPosition, BackgroundRepeat, BackgroundSize, Band,
45    Border, BorderCollapse, BorderRadius, BorderStyle, BoxDecorationBreak, Break, Color,
46    ColumnRule, ColumnSpan, Columns, ComputedStyle, Content, ContentPiece, Coord, Corner,
47    CornerRadius, CounterStyle, DecorationLine, DecorationStyle, Edge, Edges, Family, Figures,
48    FontStyle, FontVariantAlternates, FontVariantCaps, FontVariantLigatures, FontVariantNumeric,
49    Fractions, Hyphens, Inset, Length, LineHeight, ListStyleType, MarginBox, NumericSpacing,
50    PageGeometry, Position, ShapeOutside, ShapePoint, ShapeSource, SizeSource, StringPiece,
51    StringSet, Target, TextAlign, TextDecoration, TextJustify, TextTransform, Url, Width, WrapFlow,
52};
53pub use sheet::{FontFace, Origin, SheetPosition, Source, Src, Written};
54
55use element::{BLOCK_ELEMENTS, ElementTree, INLINE_ELEMENTS};
56use properties::{Custom, Declaration};
57use sheet::{Importance, MarginDeclaration, PageDeclaration, PageRule, Sheet};
58
59/// The defaults, as a stylesheet. There are no style constants in the
60/// engine; this file is where the trade paperback lives.
61pub const USER_AGENT_CSS: &str = include_str!("style/ua.css");
62
63/// Resolves `@font-face` sources to font bytes.
64///
65/// The engine reads no paths of its own. Whatever string the sheet
66/// writes in `url()` is handed over as it is: it never has to be a
67/// real URL, and a host that resolves nothing is a host with no
68/// author fonts.
69pub trait FontLoader {
70    /// The bytes behind one `src` url, or `None` when the host cannot
71    /// resolve it.
72    fn load(&self, url: &str) -> Option<Vec<u8>>;
73}
74
75/// A loader that resolves nothing.
76pub struct NoFonts;
77
78impl FontLoader for NoFonts {
79    fn load(&self, _url: &str) -> Option<Vec<u8>> {
80        None
81    }
82}
83
84/// One node's place in the tree: what it is, and which style it got.
85#[derive(Debug, Clone, PartialEq, Serialize)]
86pub struct NodeStyle {
87    /// The content node this style belongs to.
88    pub id: u32,
89    /// The element name selectors matched against.
90    pub element: &'static str,
91    /// Index into the tree's distinct styles.
92    pub style: u32,
93    /// Index of the style `::first-letter` computed for this node,
94    /// when a rule named one.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub first_letter: Option<u32>,
97    /// Index of the style `::first-line` computed for this node, when
98    /// a rule named one.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub first_line: Option<u32>,
101    /// Index of the style `::before` computed for this node, when it
102    /// generates text.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub before: Option<u32>,
105    /// Index of the style `::after` computed for this node, when it
106    /// generates text.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub after: Option<u32>,
109}
110
111/// The situation a page finds itself in, which is what `@page`
112/// selects on beyond the page's name.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
114#[serde(rename_all = "snake_case")]
115pub enum Situation {
116    /// The page a page group opens on: `@page :first`.
117    First(Side),
118    /// Any later page of the group.
119    Body(Side),
120    /// A page inserted to square the sheet: `@page :blank`.
121    Blank,
122}
123
124impl Situation {
125    /// The side of the spread this situation falls on. Blanks are
126    /// versos: a book never opens a leaf to a blank right-hand page.
127    pub fn side(self) -> Side {
128        match self {
129            Situation::First(side) | Situation::Body(side) => side,
130            Situation::Blank => Side::Verso,
131        }
132    }
133}
134
135/// Which page a `@page` lookup is about.
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct PageQuery<'a> {
138    /// The named page in force, from the `page` property.
139    pub name: Option<&'a str>,
140    /// Which page of the group, and which side.
141    pub situation: Situation,
142}
143
144/// One resolved page master: the page box, and what its margin boxes
145/// paint.
146#[derive(Debug, Clone, PartialEq, Serialize)]
147pub struct PageStyle {
148    /// Trim size and margins.
149    pub geometry: PageGeometry,
150    /// What is painted behind the whole page box, margins included.
151    #[serde(skip_serializing_if = "properties::no_background")]
152    pub background: Background,
153    /// The margin boxes the page's rules mentioned, in CSS order.
154    pub boxes: Vec<MarginBoxStyle>,
155}
156
157impl PageStyle {
158    /// The margin box `which`, when it paints anything.
159    pub fn margin_box(&self, which: MarginBox) -> Option<&MarginBoxStyle> {
160        self.boxes
161            .iter()
162            .find(|box_| box_.which == which && box_.content != Content::None)
163    }
164}
165
166/// One page margin box: what it paints and the style it paints with.
167#[derive(Debug, Clone, PartialEq, Serialize)]
168pub struct MarginBoxStyle {
169    /// Which of the sixteen boxes.
170    pub which: MarginBox,
171    /// What it paints.
172    pub content: Content,
173    /// The box's own text style, inherited from the book's root the
174    /// way a block's would be.
175    pub style: ComputedStyle,
176}
177
178/// One page master, named and situated.
179#[derive(Debug, Clone, PartialEq, Serialize)]
180pub struct PageMaster {
181    /// The named page, or `None` for the unnamed default.
182    pub page: Option<String>,
183    /// The situation this master answers.
184    pub situation: Situation,
185    /// The page box and its margin boxes.
186    pub style: PageStyle,
187}
188
189/// The compiled styling of one book.
190#[derive(Debug, Clone, Serialize)]
191pub struct StyleTree {
192    /// The distinct computed styles this book uses. Nodes index into
193    /// it: a book has thousands of nodes and a handful of styles.
194    styles: Vec<ComputedStyle>,
195    /// Every element, in document order.
196    nodes: Vec<NodeStyle>,
197    /// Every master a page of this book can take.
198    masters: Vec<PageMaster>,
199    /// Style index by raw node id. Index 0 is the root, which is also
200    /// what an unassigned node resolves to.
201    #[serde(skip)]
202    by_node: Vec<u32>,
203    /// `::first-letter` style index by raw node id, where there is one.
204    #[serde(skip)]
205    initial_by_node: Vec<Option<u32>>,
206    /// `::first-line` style index by raw node id, where there is one.
207    #[serde(skip)]
208    first_line_by_node: Vec<Option<u32>>,
209    /// `::before` style index by raw node id, where it generates text.
210    #[serde(skip)]
211    before_by_node: Vec<Option<u32>>,
212    /// `::after` style index by raw node id, where it generates text.
213    #[serde(skip)]
214    after_by_node: Vec<Option<u32>>,
215    /// Whether generated text prints the page an element lands on.
216    #[serde(skip)]
217    counts_pages: bool,
218    /// Whether generated text names another element at all.
219    #[serde(skip)]
220    refers: bool,
221    /// The footnote area, which no content node stands behind.
222    #[serde(skip)]
223    notes: NodeId,
224    warnings: Vec<Warning>,
225}
226
227impl StyleTree {
228    /// The computed style of one content node, or of one
229    /// pseudo-element. An id the tree does not know — an unassigned
230    /// one, or a node from another book — gets the root style, which
231    /// is the book's own defaults.
232    pub fn style(&self, id: NodeId) -> &ComputedStyle {
233        if let Some((element, which)) = id.pseudo_element() {
234            return self
235                .pseudo_style(element, which)
236                .unwrap_or_else(|| self.style(NodeId::UNASSIGNED));
237        }
238        let index = self
239            .by_node
240            .get(id.get() as usize)
241            .copied()
242            .unwrap_or_default();
243        &self.styles[index as usize]
244    }
245
246    /// The node the footnote area is styled by: the `notes` element,
247    /// which stands for the foot of every page.
248    pub fn notes_area(&self) -> NodeId {
249        self.notes
250    }
251
252    /// Everything line layout needs about one node.
253    pub fn paragraph(&self, id: NodeId) -> ParagraphStyle {
254        self.style(id).paragraph()
255    }
256
257    /// The style `::first-letter` computed for one node, when a rule
258    /// named one. This is where a drop cap's size and face come from.
259    pub fn first_letter(&self, id: NodeId) -> Option<&ComputedStyle> {
260        let index = (*self.initial_by_node.get(id.get() as usize)?)?;
261        Some(&self.styles[index as usize])
262    }
263
264    /// The style `::first-line` computed for one node, when a rule
265    /// named one. Line layout reads it as what the opening line
266    /// changes about the paragraph around it.
267    pub fn first_line(&self, id: NodeId) -> Option<&ComputedStyle> {
268        let index = (*self.first_line_by_node.get(id.get() as usize)?)?;
269        Some(&self.styles[index as usize])
270    }
271
272    /// What `::first-line` changes about one node's own style, which
273    /// is what line layout applies to the opening line.
274    pub fn opening_line(&self, id: NodeId) -> Option<FirstLine> {
275        Some(self.first_line(id)?.first_line_over(self.style(id)))
276    }
277
278    /// The style `::before` computed for one node, when its `content`
279    /// generates anything. On an inline that is text inside the line.
280    /// On a block it is a box, the first child of the block.
281    pub fn before(&self, id: NodeId) -> Option<&ComputedStyle> {
282        let index = (*self.before_by_node.get(id.get() as usize)?)?;
283        Some(&self.styles[index as usize])
284    }
285
286    /// The same for `::after`.
287    pub fn after(&self, id: NodeId) -> Option<&ComputedStyle> {
288        let index = (*self.after_by_node.get(id.get() as usize)?)?;
289        Some(&self.styles[index as usize])
290    }
291
292    /// The id of one pseudo-element of `element`, where a rule styles
293    /// it. On a block, `::before` and `::after` are boxes. On an
294    /// inline element, they are text inside the line. The id comes
295    /// from the element and the pseudo-element alone, so it is the
296    /// same in every layout of the same book and sheets, and no
297    /// content node has it. `style` answers for it.
298    pub fn pseudo_element(&self, element: NodeId, which: PseudoElement) -> Option<NodeId> {
299        self.pseudo_style(element, which)
300            .map(|_| element.pseudo(which))
301    }
302
303    fn pseudo_style(&self, element: NodeId, which: PseudoElement) -> Option<&ComputedStyle> {
304        match which {
305            PseudoElement::Before => self.before(element),
306            PseudoElement::After => self.after(element),
307            PseudoElement::FirstLetter => self.first_letter(element),
308            PseudoElement::FirstLine => self.first_line(element),
309        }
310    }
311
312    /// Whether any generated text prints the page another element
313    /// lands on. Such a book is laid out twice: once to find the
314    /// pages, and once to print them.
315    pub fn counts_pages(&self) -> bool {
316        self.counts_pages
317    }
318
319    /// Whether any generated text names another element, for its
320    /// page or for its text.
321    pub fn refers(&self) -> bool {
322        self.refers
323    }
324
325    /// The style of the book itself: the root of inheritance.
326    pub fn root(&self) -> &ComputedStyle {
327        &self.styles[0]
328    }
329
330    /// The distinct computed styles, in the order nodes index them.
331    pub fn styles(&self) -> &[ComputedStyle] {
332        &self.styles
333    }
334
335    /// Every element, in document order.
336    pub fn nodes(&self) -> &[NodeStyle] {
337        &self.nodes
338    }
339
340    /// What compilation had to complain about.
341    pub fn warnings(&self) -> &[Warning] {
342        &self.warnings
343    }
344
345    /// The page master one page resolves to. A named page with no
346    /// master of its own falls back to the unnamed one.
347    pub fn page(&self, query: PageQuery<'_>) -> &PageStyle {
348        self.master(query.name, query.situation)
349            .or_else(|| self.master(None, query.situation))
350            .expect("every situation has an unnamed master")
351    }
352
353    /// The default page: an unnamed body recto. The measure every
354    /// paragraph breaks to comes from here.
355    pub fn default_page(&self) -> &PageStyle {
356        self.page(PageQuery {
357            name: None,
358            situation: Situation::Body(Side::Recto),
359        })
360    }
361
362    /// Every master this book can put on a page: one per named page
363    /// and situation, in a stable order.
364    pub fn masters(&self) -> &[PageMaster] {
365        &self.masters
366    }
367
368    fn master(&self, name: Option<&str>, situation: Situation) -> Option<&PageStyle> {
369        self.masters
370            .iter()
371            .find(|master| master.page.as_deref() == name && master.situation == situation)
372            .map(|master| &master.style)
373    }
374}
375
376/// Resolves every master the book can use: the unnamed default, plus
377/// each named page, in each situation a page can be in.
378fn resolve_masters(
379    pages: &[(u8, PageRule)],
380    styles: &[ComputedStyle],
381    root: &ComputedStyle,
382    registry: &FontRegistry,
383    warnings: &mut Vec<Warning>,
384) -> Vec<PageMaster> {
385    let mut names: Vec<Option<String>> = vec![None];
386    for style in styles {
387        if let Some(name) = &style.page
388            && !names.iter().any(|known| known.as_deref() == Some(name))
389        {
390            names.push(Some(name.clone()));
391        }
392    }
393    let situations = [
394        Situation::First(Side::Recto),
395        Situation::First(Side::Verso),
396        Situation::Body(Side::Recto),
397        Situation::Body(Side::Verso),
398        Situation::Blank,
399    ];
400    let mut masters = Vec::new();
401    for name in names {
402        for situation in situations {
403            masters.push(PageMaster {
404                page: name.clone(),
405                situation,
406                style: resolve_page(
407                    pages,
408                    PageQuery {
409                        name: name.as_deref(),
410                        situation,
411                    },
412                    root,
413                    registry,
414                    warnings,
415                ),
416            });
417        }
418    }
419    masters
420}
421
422/// One page master: every `@page` rule that selects it, applied in
423/// specificity then source order.
424fn resolve_page(
425    pages: &[(u8, PageRule)],
426    query: PageQuery<'_>,
427    root: &ComputedStyle,
428    registry: &FontRegistry,
429    warnings: &mut Vec<Warning>,
430) -> PageStyle {
431    let mut matching: Vec<&(u8, PageRule)> = pages
432        .iter()
433        .filter(|(_, rule)| selects(rule, query))
434        .collect();
435    // `@page` cascades like any other rule: origin first, then
436    // specificity. The rules are already in source order, and a stable
437    // sort leaves the later of two equal rules on top.
438    matching.sort_by_key(|(level, rule)| (*level, rule.specificity()));
439
440    let root_size = root.font_size;
441    // `column-gap: normal` is one em of the book's own size, and the
442    // page box is not text, so the em it means is the root's.
443    let mut geometry = PageGeometry {
444        width: 612.0,
445        height: 792.0,
446        margin: Edges::all(0.0),
447        columns: Columns::undivided(root_size),
448        align_content: AlignContent::Start,
449    };
450    let initial = geometry;
451    let mut background = Background::NONE;
452    let mut boxes: BTreeMap<MarginBox, MarginBoxStyle> = BTreeMap::new();
453    for (_, rule) in matching {
454        for declaration in &rule.declarations {
455            let PageDeclaration::Pending(pending) = declaration else {
456                apply_page(&mut geometry, &mut background, declaration, root_size);
457                continue;
458            };
459            let read = sheet::read_pending_page;
460            match substitution::substituted(pending, &root.custom, read, warnings) {
461                Some(declarations) => {
462                    for declaration in &declarations {
463                        apply_page(&mut geometry, &mut background, declaration, root_size);
464                    }
465                }
466                None => {
467                    for like in sheet::page_longhands(&pending.property) {
468                        reset_page(&mut geometry, &mut background, &like, &initial);
469                    }
470                }
471            }
472        }
473        for margin in &rule.boxes {
474            let entry = boxes.entry(margin.which).or_insert_with(|| MarginBoxStyle {
475                which: margin.which,
476                content: Content::None,
477                style: root.inherit(),
478            });
479            for declaration in &margin.declarations {
480                let MarginDeclaration::Pending(pending) = declaration else {
481                    apply_margin(entry, declaration, root_size);
482                    continue;
483                };
484                let read = sheet::read_pending_margin;
485                match substitution::substituted(pending, &root.custom, read, warnings) {
486                    Some(declarations) => {
487                        for declaration in &declarations {
488                            apply_margin(entry, declaration, root_size);
489                        }
490                    }
491                    None => {
492                        let base = root.inherit();
493                        for like in sheet::margin_longhands(&pending.property) {
494                            match like {
495                                MarginDeclaration::Style(like) => entry.style.reset(&like, &base),
496                                _ => entry.content = Content::None,
497                            }
498                        }
499                    }
500                }
501            }
502            let (font_id, warning) = resolve_face(&entry.style, registry);
503            entry.style.font_id = font_id;
504            report(warnings, warning);
505        }
506    }
507    PageStyle {
508        geometry,
509        background,
510        boxes: boxes.into_values().collect(),
511    }
512}
513
514/// Applies one declaration of a `@page` body.
515fn apply_page(
516    geometry: &mut PageGeometry,
517    background: &mut Background,
518    declaration: &PageDeclaration,
519    root_size: f32,
520) {
521    match declaration {
522        PageDeclaration::Size(width, height) => {
523            geometry.width = *width;
524            geometry.height = *height;
525        }
526        PageDeclaration::Margin(edge, length) => {
527            *geometry.margin.edge(*edge) = length.to_points(root_size, root_size)
528        }
529        PageDeclaration::BackgroundColor(color) => background.color = *color,
530        PageDeclaration::BackgroundImage(url) => background.image = url.clone(),
531        PageDeclaration::BackgroundRepeat(repeat) => background.repeat = *repeat,
532        PageDeclaration::BackgroundSize(size) => {
533            background.size = properties::computed_size(*size, root_size, root_size)
534        }
535        PageDeclaration::BackgroundPosition(x, y) => {
536            background.position = BackgroundPosition {
537                x: Coord::of(*x, root_size, root_size),
538                y: Coord::of(*y, root_size, root_size),
539            }
540        }
541        PageDeclaration::ColumnCount(count) => geometry.columns.count = *count,
542        PageDeclaration::ColumnWidth(width) => {
543            geometry.columns.width = width.map(|width| width.to_points(root_size, root_size))
544        }
545        PageDeclaration::ColumnGap(gap) => {
546            geometry.columns.gap = gap
547                .map(|gap| gap.to_points(root_size, root_size))
548                .unwrap_or(root_size)
549                .max(0.0)
550        }
551        PageDeclaration::ColumnRuleWidth(width) => {
552            geometry.columns.rule.width = width.to_points(root_size, root_size)
553        }
554        PageDeclaration::ColumnRuleStyle(style) => geometry.columns.rule.style = *style,
555        PageDeclaration::AlignContent(align) => geometry.align_content = *align,
556        PageDeclaration::Pending(_) => {}
557    }
558}
559
560/// Sets what `like` declares on a page back to its initial value.
561fn reset_page(
562    geometry: &mut PageGeometry,
563    background: &mut Background,
564    like: &PageDeclaration,
565    initial: &PageGeometry,
566) {
567    match like {
568        PageDeclaration::Size(..) => {
569            geometry.width = initial.width;
570            geometry.height = initial.height;
571        }
572        PageDeclaration::Margin(edge, _) => {
573            *geometry.margin.edge(*edge) = initial.margin.get(*edge)
574        }
575        PageDeclaration::BackgroundColor(_) => background.color = Background::NONE.color,
576        PageDeclaration::BackgroundImage(_) => background.image = None,
577        PageDeclaration::BackgroundRepeat(_) => background.repeat = Background::NONE.repeat,
578        PageDeclaration::BackgroundSize(_) => background.size = Background::NONE.size,
579        PageDeclaration::BackgroundPosition(..) => background.position = Background::NONE.position,
580        PageDeclaration::ColumnCount(_) => geometry.columns.count = initial.columns.count,
581        PageDeclaration::ColumnWidth(_) => geometry.columns.width = initial.columns.width,
582        PageDeclaration::ColumnGap(_) => geometry.columns.gap = initial.columns.gap,
583        PageDeclaration::ColumnRuleWidth(_) => {
584            geometry.columns.rule.width = initial.columns.rule.width
585        }
586        PageDeclaration::ColumnRuleStyle(_) => {
587            geometry.columns.rule.style = initial.columns.rule.style
588        }
589        PageDeclaration::AlignContent(_) => geometry.align_content = initial.align_content,
590        PageDeclaration::Pending(_) => {}
591    }
592}
593
594/// Applies one declaration of a page margin box.
595fn apply_margin(entry: &mut MarginBoxStyle, declaration: &MarginDeclaration, root_size: f32) {
596    match declaration {
597        MarginDeclaration::Content(content) => entry.content = content.clone(),
598        MarginDeclaration::Style(style) => entry.style.apply(style, root_size, root_size),
599        MarginDeclaration::Pending(_) => {}
600    }
601}
602
603impl InlineStyles for StyleTree {
604    fn style(&self, id: NodeId, block: &ParagraphStyle) -> ParagraphStyle {
605        match self.by_node.get(id.get() as usize) {
606            Some(index) => self.styles[*index as usize].paragraph(),
607            None => block.clone(),
608        }
609    }
610
611    fn inline_box(&self, id: NodeId) -> Option<InlineBox> {
612        let index = self.by_node.get(id.get() as usize)?;
613        self.styles[*index as usize].inline_box()
614    }
615}
616
617/// Whether one `@page` rule selects the page a query describes.
618fn selects(rule: &PageRule, query: PageQuery<'_>) -> bool {
619    if rule.name.is_some() && rule.name.as_deref() != query.name {
620        return false;
621    }
622    if rule.first && !matches!(query.situation, Situation::First(_)) {
623        return false;
624    }
625    if rule.blank && query.situation != Situation::Blank {
626        return false;
627    }
628    match rule.side {
629        Some(side) => side == query.situation.side(),
630        None => true,
631    }
632}
633
634/// Parsed stylesheets: the built-in sheet, then whatever the host
635/// added, ready to compile against a book.
636///
637/// Parsing is separate from compiling because a run styles several
638/// books from one set of sheets, and separate from font loading
639/// because loading is the one step that reaches outside the engine.
640#[derive(Debug)]
641pub struct Stylesheets {
642    sheets: Vec<Sheet>,
643    /// What parsing said, then what font loading said.
644    warnings: Vec<Warning>,
645    /// How many of `warnings` parsing raised.
646    parsed: usize,
647}
648
649impl Stylesheets {
650    /// Parses the built-in sheet, then `sources` in the order given.
651    pub fn parse(sources: &[Source<'_>]) -> Stylesheets {
652        let built_in = Source::user_agent("user-agent.css", USER_AGENT_CSS);
653        let mut sheets = Vec::new();
654        let mut warnings = Vec::new();
655        for source in std::iter::once(&built_in).chain(sources) {
656            let (sheet, mut sheet_warnings) = sheet::parse(source);
657            warnings.append(&mut sheet_warnings);
658            sheets.push(sheet);
659        }
660        let parsed = warnings.len();
661        Stylesheets {
662            sheets,
663            warnings,
664            parsed,
665        }
666    }
667
668    /// Registers every `@font-face` the loader resolves, under the
669    /// family the sheet gave it rather than the one inside the file:
670    /// a stylesheet's name for a face is the one its selectors use.
671    ///
672    /// Run this before compiling — a face the registry does not have
673    /// is a face no computed style can resolve to. Running it again
674    /// replaces what the last run warned about.
675    pub fn load_fonts(&mut self, registry: &mut FontRegistry, loader: &dyn FontLoader) {
676        let warnings = self
677            .sheets
678            .iter()
679            .flat_map(|sheet| &sheet.faces)
680            .flat_map(|face| register_face(face, registry, loader))
681            .collect();
682        self.set_font_warnings(warnings);
683    }
684
685    /// What the last font loading warned about.
686    pub(crate) fn font_warnings(&self) -> &[Warning] {
687        &self.warnings[self.parsed..]
688    }
689
690    /// Replaces what font loading warned about, for sheets whose faces
691    /// a session already registered from an earlier copy of them.
692    pub(crate) fn set_font_warnings(&mut self, warnings: Vec<Warning>) {
693        self.warnings.truncate(self.parsed);
694        self.warnings.extend(warnings);
695    }
696
697    /// Hashes every `@font-face` the sheets declare, and what `file`
698    /// says about the bytes behind each url: two sheets that hash the
699    /// same register the same faces.
700    pub(crate) fn hash_faces(
701        &self,
702        h: &mut impl std::hash::Hasher,
703        file: impl Fn(&str) -> Option<u64>,
704    ) {
705        use std::hash::Hash;
706        for face in self.sheets.iter().flat_map(|sheet| &sheet.faces) {
707            face.family.hash(h);
708            face.style.hash(h);
709            face.weight.hash(h);
710            for src in &face.src {
711                match src {
712                    Src::Url(url) => (url, file(url)).hash(h),
713                    Src::Local(name) => name.hash(h),
714                }
715            }
716        }
717    }
718
719    /// What parsing and font loading had to complain about.
720    pub fn warnings(&self) -> &[Warning] {
721        &self.warnings
722    }
723
724    /// Every sheet as it was written, the built-in sheet first: what
725    /// a writer reads that hands the CSS to a renderer of its own
726    /// rather than laying it out.
727    ///
728    /// Only what the parser accepted is here. A rule or a declaration
729    /// outside the subset is in [`Stylesheets::warnings`] instead.
730    pub fn written(&self) -> Vec<WrittenSheet> {
731        self.sheets
732            .iter()
733            .map(|sheet| WrittenSheet {
734                origin: sheet.origin,
735                rules: sheet
736                    .rules
737                    .iter()
738                    .map(|rule| WrittenRule {
739                        selectors: rule
740                            .selectors
741                            .slice()
742                            .iter()
743                            .map(|selector| selector.to_css_string())
744                            .collect(),
745                        declarations: rule.written.clone(),
746                        position: rule.position.clone(),
747                    })
748                    .collect(),
749                pages: sheet
750                    .pages
751                    .iter()
752                    .map(|page| WrittenPage {
753                        selector: page.selector.clone(),
754                        position: page.position.clone(),
755                    })
756                    .collect(),
757                faces: sheet.faces.clone(),
758            })
759            .collect()
760    }
761
762    /// Compiles one book's styling against the faces in the registry.
763    pub fn compile(&self, book: &Book, registry: &FontRegistry) -> StyleTree {
764        cascade(book, &self.sheets, registry, self.warnings.clone())
765    }
766}
767
768/// One sheet as it was written. See [`Stylesheets::written`].
769#[derive(Debug, Clone, PartialEq)]
770pub struct WrittenSheet {
771    /// Which origin the sheet cascades at.
772    pub origin: Origin,
773    /// The style rules, in the order they were written.
774    pub rules: Vec<WrittenRule>,
775    /// The `@page` rules, in the order they were written.
776    pub pages: Vec<WrittenPage>,
777    /// The `@font-face` rules, in the order they were written.
778    pub faces: Vec<FontFace>,
779}
780
781/// One style rule as it was written.
782#[derive(Debug, Clone, PartialEq)]
783pub struct WrittenRule {
784    /// Each selector of the list, as CSS.
785    pub selectors: Vec<String>,
786    /// The declarations the parser accepted, in the order they were
787    /// written.
788    pub declarations: Vec<Written>,
789    /// Where the rule begins.
790    pub position: SheetPosition,
791}
792
793/// One `@page` rule as it was written.
794#[derive(Debug, Clone, PartialEq)]
795pub struct WrittenPage {
796    /// The prelude, `@page` included.
797    pub selector: String,
798    /// Where the rule begins.
799    pub position: SheetPosition,
800}
801
802/// One book's styling under the built-in sheet alone.
803pub fn defaults(book: &Book, registry: &FontRegistry) -> StyleTree {
804    Stylesheets::parse(&[]).compile(book, registry)
805}
806
807/// Registers one `@font-face`, or says why it could not be.
808fn register_face(
809    face: &FontFace,
810    registry: &mut FontRegistry,
811    loader: &dyn FontLoader,
812) -> Vec<Warning> {
813    let mut warnings = Vec::new();
814    for src in &face.src {
815        let Src::Url(url) = src else {
816            continue;
817        };
818        let Some(bytes) = loader.load(url) else {
819            continue;
820        };
821        match FontSource::from_bytes(bytes) {
822            Ok(mut source) => {
823                source.family = face.family.to_lowercase();
824                source.declared = declared(face);
825                return match registry.add(source) {
826                    Ok(_) => warnings,
827                    Err(error) => {
828                        warnings.push(Warning {
829                            message: face_failure(&face.family, &error),
830                            origin: Some(url.clone()),
831                        });
832                        warnings
833                    }
834                };
835            }
836            Err(error) => warnings.push(Warning {
837                message: face_failure(&face.family, &error),
838                origin: Some(url.clone()),
839            }),
840        }
841    }
842    warnings.push(Warning {
843        message: format!("No src url loaded for {}. {FALLBACK}", face.family),
844        origin: None,
845    });
846    warnings
847}
848
849/// What happens to text that asked for a face the registry has not
850/// got.
851const FALLBACK: &str = "Falling back to another registered font.";
852
853/// Why one `@font-face` file did not register. The two causes read
854/// apart, because the fix for each one is different.
855fn face_failure(family: &str, error: &FontError) -> String {
856    match error {
857        FontError::MissingName => {
858            format!("{family} has no family name and was not registered. {FALLBACK}")
859        }
860        FontError::Parse => format!("{family} could not be read as a font. {FALLBACK}"),
861    }
862}
863
864/// What a `@font-face` declared its source to be. A sheet that
865/// declares neither slope nor weight is naming a family, not a cut,
866/// and the file names the cuts.
867fn declared(face: &FontFace) -> Option<FaceAttributes> {
868    let (style, weight) = (face.style, face.weight);
869    (style.is_some() || weight.is_some()).then(|| FaceAttributes {
870        italic: style == Some(FontStyle::Italic),
871        weight: weight.unwrap_or(FaceAttributes::REGULAR.weight),
872    })
873}
874
875/// Matching and the cascade: every element gets the declarations that
876/// match it, in cascade order, applied over what it inherited.
877fn cascade(
878    book: &Book,
879    sheets: &[Sheet],
880    registry: &FontRegistry,
881    mut warnings: Vec<Warning>,
882) -> StyleTree {
883    let elements = ElementTree::build(book);
884    warnings.extend(repeated_ids(book));
885    let mut caches = SelectorCaches::default();
886
887    let mut styles: Vec<ComputedStyle> = Vec::new();
888    let mut nodes = Vec::new();
889    let mut computed: Vec<u32> = Vec::with_capacity(elements.nodes().len());
890    let mut first_letters: Vec<Option<u32>> = Vec::with_capacity(elements.nodes().len());
891    let mut first_lines: Vec<Option<u32>> = Vec::with_capacity(elements.nodes().len());
892    let mut befores: Vec<Option<u32>> = Vec::with_capacity(elements.nodes().len());
893    let mut afters: Vec<Option<u32>> = Vec::with_capacity(elements.nodes().len());
894    let mut max_id = 0u32;
895    // Most sheets name neither, and a pass that no rule can match is
896    // a pass over every rule for every element.
897    let named = |which: &PseudoElement| {
898        sheets.iter().any(|sheet| {
899            sheet.rules.iter().any(|rule| {
900                rule.selectors
901                    .slice()
902                    .iter()
903                    .any(|selector| selector.pseudo_element() == Some(which))
904            })
905        })
906    };
907    let (wants_before, wants_after) = (named(&PseudoElement::Before), named(&PseudoElement::After));
908
909    for (index, node) in elements.nodes().iter().enumerate() {
910        let parent = node
911            .parent
912            .map(|parent| styles[computed[parent] as usize].clone())
913            .unwrap_or_else(ComputedStyle::initial);
914        let mut style = parent.inherit();
915
916        let matched = applicable(sheets, &elements, index, &mut caches, None);
917        let root_size = styles.first().map(|style| style.font_size);
918        let parent_size = parent.font_size;
919        let hint = node.align.map(align_hint);
920        apply_all(
921            &mut style,
922            sheets,
923            &matched,
924            hint.as_ref(),
925            parent_size,
926            root_size,
927            &mut warnings,
928        );
929        let (font_id, warning) = resolve_face(&style, registry);
930        style.font_id = font_id;
931        report(&mut warnings, warning);
932        report(&mut warnings, synthesized_small_caps(&style, registry));
933        report_all(&mut warnings, missing_features(&style, registry));
934
935        // A pseudo-element cascades over the style of the element it
936        // belongs to, so each is a second matching pass rather than a
937        // second element.
938        let mut pseudo_style = |which, warnings: &mut Vec<Warning>| {
939            let matched = applicable(sheets, &elements, index, &mut caches, Some(which));
940            (!matched.is_empty()).then(|| {
941                let mut pseudo = style.inherit();
942                apply_all(
943                    &mut pseudo,
944                    sheets,
945                    &matched,
946                    None,
947                    style.font_size,
948                    root_size,
949                    warnings,
950                );
951                let (font_id, warning) = resolve_face(&pseudo, registry);
952                pseudo.font_id = font_id;
953                report(warnings, warning);
954                report(warnings, synthesized_small_caps(&pseudo, registry));
955                report_all(warnings, missing_features(&pseudo, registry));
956                pseudo
957            })
958        };
959        let first_letter = pseudo_style(&PseudoElement::FirstLetter, &mut warnings);
960        let first_line = pseudo_style(&PseudoElement::FirstLine, &mut warnings);
961        let before = wants_before
962            .then(|| pseudo_style(&PseudoElement::Before, &mut warnings))
963            .flatten();
964        let after = wants_after
965            .then(|| pseudo_style(&PseudoElement::After, &mut warnings))
966            .flatten();
967        // `content: none`, the initial value, generates nothing.
968        let [before, after] = [(before, "::before"), (after, "::after")].map(|(pseudo, name)| {
969            let pseudo = pseudo.filter(|pseudo| pseudo.content != Content::None)?;
970            if INLINE_ELEMENTS.contains(&node.name) || BLOCK_ELEMENTS.contains(&node.name) {
971                return Some(pseudo);
972            }
973            let message = format!(
974                "Unsupported pseudo-element `{name}` on `{}`. Nothing is generated.",
975                node.name
976            );
977            if !warnings.iter().any(|seen| seen.message == message) {
978                warnings.push(Warning {
979                    message,
980                    origin: None,
981                });
982            }
983            None
984        });
985
986        let index_of_style = intern(&mut styles, style);
987        let index_of_initial = first_letter.map(|initial| intern(&mut styles, initial));
988        let index_of_line = first_line.map(|line| intern(&mut styles, line));
989        let index_of_before = before.map(|before| intern(&mut styles, before));
990        let index_of_after = after.map(|after| intern(&mut styles, after));
991        computed.push(index_of_style);
992        first_letters.push(index_of_initial);
993        first_lines.push(index_of_line);
994        befores.push(index_of_before);
995        afters.push(index_of_after);
996        nodes.push(NodeStyle {
997            id: node.id.get(),
998            element: node.name,
999            style: index_of_style,
1000            first_letter: index_of_initial,
1001            first_line: index_of_line,
1002            before: index_of_before,
1003            after: index_of_after,
1004        });
1005        max_id = max_id.max(node.id.get());
1006    }
1007
1008    // A paragraph with no element of its own takes the style of an
1009    // anonymous box inside its item: what the item passes on, and
1010    // nothing of its own.
1011    let mut anonymous = Vec::with_capacity(elements.anonymous().len());
1012    for (id, item) in elements.anonymous() {
1013        let inherited = styles[computed[*item] as usize].inherit();
1014        anonymous.push((id.get() as usize, intern(&mut styles, inherited)));
1015        max_id = max_id.max(id.get());
1016    }
1017
1018    let root = computed.first().copied().unwrap_or(0);
1019    let mut by_node = vec![root; max_id as usize + 1];
1020    let mut initial_by_node = vec![None; max_id as usize + 1];
1021    let mut first_line_by_node = vec![None; max_id as usize + 1];
1022    let mut before_by_node = vec![None; max_id as usize + 1];
1023    let mut after_by_node = vec![None; max_id as usize + 1];
1024    for (index, node) in elements.nodes().iter().enumerate() {
1025        let id = node.id.get() as usize;
1026        by_node[id] = computed[index];
1027        initial_by_node[id] = first_letters[index];
1028        first_line_by_node[id] = first_lines[index];
1029        before_by_node[id] = befores[index];
1030        after_by_node[id] = afters[index];
1031    }
1032    for (id, style) in anonymous {
1033        by_node[id] = style;
1034    }
1035    by_node[0] = root;
1036    let generated = || {
1037        befores
1038            .iter()
1039            .chain(&afters)
1040            .flatten()
1041            .map(|index| &styles[*index as usize].content)
1042    };
1043    let counts_pages = generated().any(Content::counts_pages);
1044    let refers = generated().any(|content| matches!(content, Content::Pieces(_)));
1045
1046    if styles.is_empty() {
1047        styles.push(ComputedStyle::initial());
1048    }
1049    let pages: Vec<(u8, PageRule)> = sheets
1050        .iter()
1051        .flat_map(|sheet| {
1052            let level = level(sheet.origin, Importance::Normal);
1053            sheet.pages.iter().cloned().map(move |rule| (level, rule))
1054        })
1055        .collect();
1056    let masters = resolve_masters(
1057        &pages,
1058        &styles,
1059        &styles[root as usize].clone(),
1060        registry,
1061        &mut warnings,
1062    );
1063
1064    StyleTree {
1065        styles,
1066        nodes,
1067        masters,
1068        by_node,
1069        initial_by_node,
1070        first_line_by_node,
1071        before_by_node,
1072        after_by_node,
1073        counts_pages,
1074        refers,
1075        notes: elements.notes(),
1076        warnings,
1077    }
1078}
1079
1080/// Reports every id a book gives to a second element.
1081///
1082/// An id names one element. Both still match.
1083fn repeated_ids(book: &Book) -> Vec<Warning> {
1084    let mut first: BTreeMap<&str, String> = BTreeMap::new();
1085    let mut warnings = Vec::new();
1086    for section in &book.sections {
1087        let source = section.source.as_deref();
1088        named_blocks(&section.blocks, source, &mut first, &mut warnings);
1089    }
1090    warnings
1091}
1092
1093/// The ids of these blocks and everything inside them, in document
1094/// order, warning on each id already taken.
1095fn named_blocks<'a>(
1096    blocks: &'a [Block],
1097    source: Option<&str>,
1098    first: &mut BTreeMap<&'a str, String>,
1099    warnings: &mut Vec<Warning>,
1100) {
1101    for block in blocks {
1102        claim(
1103            block_attributes(block),
1104            origin(source, block_position(block)),
1105            first,
1106            warnings,
1107        );
1108        match block {
1109            Block::Heading { inlines, .. } | Block::Paragraph { inlines, .. } => {
1110                named_inlines(inlines, source, first, warnings)
1111            }
1112            Block::Blockquote { blocks, .. } => named_blocks(blocks, source, first, warnings),
1113            Block::List { items, .. } => {
1114                for item in items {
1115                    claim(
1116                        &item.attributes,
1117                        origin(source, item.position),
1118                        first,
1119                        warnings,
1120                    );
1121                    named_blocks(&item.blocks, source, first, warnings);
1122                }
1123            }
1124            Block::Table { head, body, .. } => {
1125                for row in rows(head, body) {
1126                    claim(
1127                        &row.attributes,
1128                        origin(source, row.position),
1129                        first,
1130                        warnings,
1131                    );
1132                    for cell in &row.cells {
1133                        claim(
1134                            &cell.attributes,
1135                            origin(source, cell.position),
1136                            first,
1137                            warnings,
1138                        );
1139                        named_blocks(&cell.blocks, source, first, warnings);
1140                    }
1141                }
1142            }
1143            Block::CodeBlock { .. }
1144            | Block::ThematicBreak { .. }
1145            | Block::PageBreak { .. }
1146            | Block::ColumnBreak { .. }
1147            | Block::Image { .. } => {}
1148        }
1149    }
1150}
1151
1152/// The same, over the inlines of one block.
1153fn named_inlines<'a>(
1154    inlines: &'a [Inline],
1155    source: Option<&str>,
1156    first: &mut BTreeMap<&'a str, String>,
1157    warnings: &mut Vec<Warning>,
1158) {
1159    for inline in inlines {
1160        claim(
1161            inline_attributes(inline),
1162            origin(source, inline_position(inline)),
1163            first,
1164            warnings,
1165        );
1166        match inline {
1167            Inline::Text { .. } | Inline::Code { .. } | Inline::Break { .. } => {}
1168            Inline::Note { blocks, .. } => named_blocks(blocks, source, first, warnings),
1169            Inline::Emphasis { children, .. }
1170            | Inline::Strong { children, .. }
1171            | Inline::Link { children, .. }
1172            | Inline::Strikethrough { children, .. }
1173            | Inline::Span { children, .. } => named_inlines(children, source, first, warnings),
1174        }
1175    }
1176}
1177
1178/// Takes one node's id, or reports the element that has it already.
1179fn claim<'a>(
1180    attributes: &'a Attributes,
1181    at: String,
1182    first: &mut BTreeMap<&'a str, String>,
1183    warnings: &mut Vec<Warning>,
1184) {
1185    let Some(id) = attributes.id.as_deref() else {
1186        return;
1187    };
1188    match first.get(id) {
1189        Some(taken) => warnings.push(Warning {
1190            message: format!("Id `{id}` is duplicated. Applied at {taken}."),
1191            origin: Some(at),
1192        }),
1193        None => {
1194            first.insert(id, at);
1195        }
1196    }
1197}
1198
1199/// Where one applicable declaration lives, sorted into cascade order:
1200/// origin and importance, then specificity, then source order.
1201type Applicable = (u8, u32, usize, usize, usize);
1202
1203/// Every declaration that matches one element, in cascade order.
1204/// `pseudo` selects the pass: `None` matches the element itself,
1205/// `Some(_)` matches only rules ending in that pseudo-element.
1206fn applicable(
1207    sheets: &[Sheet],
1208    elements: &ElementTree,
1209    index: usize,
1210    caches: &mut SelectorCaches,
1211    pseudo: Option<&PseudoElement>,
1212) -> Vec<Applicable> {
1213    let mode = match pseudo {
1214        Some(_) => MatchingMode::ForStatelessPseudoElement,
1215        None => MatchingMode::Normal,
1216    };
1217    let mut applicable = Vec::new();
1218    for (sheet_index, sheet) in sheets.iter().enumerate() {
1219        for (rule_index, rule) in sheet.rules.iter().enumerate() {
1220            let specificity = rule
1221                .selectors
1222                .slice()
1223                .iter()
1224                .filter(|selector| selector.pseudo_element() == pseudo)
1225                .filter(|selector| {
1226                    let mut context = MatchingContext::new(
1227                        mode,
1228                        None,
1229                        caches,
1230                        QuirksMode::NoQuirks,
1231                        NeedsSelectorFlags::No,
1232                        MatchingForInvalidation::No,
1233                    );
1234                    matches_selector(selector, 0, None, &elements.at(index), &mut context)
1235                })
1236                .map(|selector| selector.specificity())
1237                .max();
1238            let Some(specificity) = specificity else {
1239                continue;
1240            };
1241            for (order, (_, importance)) in rule.declarations.iter().enumerate() {
1242                applicable.push((
1243                    level(sheet.origin, *importance),
1244                    specificity,
1245                    sheet_index,
1246                    rule_index,
1247                    order,
1248                ));
1249            }
1250        }
1251    }
1252    applicable.sort_unstable();
1253    applicable
1254}
1255
1256/// Applies matched declarations in cascade order.
1257///
1258/// `hint` is what the source wrote on the element itself, as HTML's
1259/// `align` is written on a cell. It goes in after the built-in sheet
1260/// and before every rule of the author's, whatever its specificity.
1261fn apply_all(
1262    style: &mut ComputedStyle,
1263    sheets: &[Sheet],
1264    applicable: &[Applicable],
1265    hint: Option<&properties::Declaration>,
1266    parent_size: f32,
1267    root_size: Option<f32>,
1268    warnings: &mut Vec<Warning>,
1269) {
1270    let root_size = root_size.unwrap_or(parent_size);
1271    let declaration = |(_, _, sheet, rule, order): &Applicable| {
1272        &sheets[*sheet].rules[*rule].declarations[*order].0
1273    };
1274    // Custom properties resolve first, because a declaration anywhere
1275    // in the cascade can read one declared after it.
1276    let declared: Vec<&Custom> = applicable
1277        .iter()
1278        .filter_map(|at| match declaration(at) {
1279            Declaration::Custom(custom) => Some(custom),
1280            _ => None,
1281        })
1282        .collect();
1283    if !declared.is_empty() {
1284        style.custom = substitution::resolve(&style.custom, &declared, warnings);
1285    }
1286    let base = applicable
1287        .iter()
1288        .any(|at| matches!(declaration(at), Declaration::Pending(_)))
1289        .then(|| style.clone());
1290    let mut hint = hint;
1291    for at in applicable {
1292        if at.0 > 0
1293            && let Some(declaration) = hint.take()
1294        {
1295            style.apply(declaration, parent_size, root_size);
1296        }
1297        match (declaration(at), &base) {
1298            (Declaration::Pending(pending), Some(base)) => {
1299                substitution::apply(style, pending, base, parent_size, root_size, warnings)
1300            }
1301            (declaration, _) => style.apply(declaration, parent_size, root_size),
1302        }
1303    }
1304    if let Some(declaration) = hint {
1305        style.apply(declaration, parent_size, root_size);
1306    }
1307}
1308
1309/// The `text-align` a table's delimiter row wrote on a cell's column.
1310fn align_hint(align: Alignment) -> properties::Declaration {
1311    properties::Declaration::TextAlign(match align {
1312        Alignment::Left => TextAlign::Left,
1313        Alignment::Center => TextAlign::Center,
1314        Alignment::Right => TextAlign::Right,
1315    })
1316}
1317
1318/// Where one declaration sits in the cascade, before specificity is
1319/// consulted: author CSS beats the built-in sheet, and `!important`
1320/// turns that round for the origin that used it.
1321fn level(origin: Origin, importance: Importance) -> u8 {
1322    match (origin, importance) {
1323        (Origin::UserAgent, Importance::Normal) => 0,
1324        (Origin::Author, Importance::Normal) => 1,
1325        (Origin::Author, Importance::Important) => 2,
1326        (Origin::UserAgent, Importance::Important) => 3,
1327    }
1328}
1329
1330/// The index of a style in the table, adding it if it is new. Books
1331/// have thousands of nodes and a handful of styles.
1332fn intern(styles: &mut Vec<ComputedStyle>, style: ComputedStyle) -> u32 {
1333    match styles.iter().position(|known| *known == style) {
1334        Some(index) => index as u32,
1335        None => {
1336            styles.push(style);
1337            (styles.len() - 1) as u32
1338        }
1339    }
1340}
1341
1342/// Records a face diagnostic once. Thousands of nodes share a
1343/// handful of styles, and a font stack that cannot be honoured is a
1344/// fact about the stack, not about each node that used it.
1345fn report(warnings: &mut Vec<Warning>, warning: Option<Warning>) {
1346    let Some(warning) = warning else {
1347        return;
1348    };
1349    if !warnings.iter().any(|seen| seen.message == warning.message) {
1350        warnings.push(warning);
1351    }
1352}
1353
1354/// Records each of a list of diagnostics once.
1355fn report_all(warnings: &mut Vec<Warning>, found: Vec<Warning>) {
1356    for warning in found {
1357        report(warnings, Some(warning));
1358    }
1359}
1360
1361/// The face a computed style shapes with: the first family in the
1362/// registry, at the nearest slope and weight it has.
1363///
1364/// A family the registry does not have is skipped — that is what a
1365/// font stack is for — but a stack that resolves nothing, or a
1366/// family with no cut at the slope asked for, falls back to a face
1367/// that is visibly not the one requested. Both say so.
1368fn resolve_face(style: &ComputedStyle, registry: &FontRegistry) -> (u16, Option<Warning>) {
1369    let want = FaceAttributes {
1370        italic: style.font_style == FontStyle::Italic,
1371        weight: style.font_weight,
1372    };
1373    for family in &style.font_family {
1374        let name = match family {
1375            Family::Named(name) => Some(name.clone()),
1376            Family::Generic(generic) => registry
1377                .generic(*generic)
1378                .and_then(|id| registry.font_ref(id).map(|entry| entry.family.clone())),
1379        };
1380        let Some(found) = name.as_deref().and_then(|name| registry.select(name, want)) else {
1381            continue;
1382        };
1383        let warning = (found.attributes.italic != want.italic).then(|| Warning {
1384            message: format!(
1385                "{} has no {} face. {} text falls back to {}.",
1386                name.unwrap_or_default(),
1387                slope(want.italic),
1388                sentence(slope(want.italic)),
1389                slope(found.attributes.italic),
1390            ),
1391            origin: None,
1392        });
1393        return (found.id, warning);
1394    }
1395    (
1396        0,
1397        Some(Warning {
1398            message: format!(
1399                "No registered font matches {}. The first registered font is used instead.",
1400                stack(&style.font_family),
1401            ),
1402            origin: None,
1403        }),
1404    )
1405}
1406
1407/// The diagnostic for a face with no small capitals of its own, one
1408/// per style that asks for them: an author who picked the face can
1409/// pick another one.
1410fn synthesized_small_caps(style: &ComputedStyle, registry: &FontRegistry) -> Option<Warning> {
1411    let asked = style.font_variant_caps == FontVariantCaps::SmallCaps;
1412    (asked && !registry.has_small_caps(style.font_id)).then(|| Warning {
1413        message: format!(
1414            "{} has no small capitals. Falling back to reduced capitals.",
1415            registry
1416                .font_ref(style.font_id)
1417                .map(|entry| entry.family.clone())
1418                .unwrap_or_else(|| stack(&style.font_family)),
1419        ),
1420        origin: None,
1421    })
1422}
1423
1424/// The diagnostic for a face that carries none of a feature the
1425/// sheet asked for, one per style that asks for it. The run is set
1426/// without the feature; nothing is synthesized.
1427///
1428/// A setting of zero asks for a feature to be left off, which a face
1429/// that has nothing for it already does.
1430fn missing_features(style: &ComputedStyle, registry: &FontRegistry) -> Vec<Warning> {
1431    let family = || {
1432        registry
1433            .font_ref(style.font_id)
1434            .map(|entry| entry.family.clone())
1435            .unwrap_or_else(|| stack(&style.font_family))
1436    };
1437    style
1438        .features()
1439        .iter()
1440        .filter(|setting| setting.value != 0)
1441        .filter(|setting| !registry.has_feature(style.font_id, setting.tag))
1442        .map(|setting| Warning {
1443            message: format!(
1444                "{} has no `{}` feature. The text is set without it.",
1445                family(),
1446                String::from_utf8_lossy(&setting.tag),
1447            ),
1448            origin: None,
1449        })
1450        .collect()
1451}
1452
1453/// A slope as a stylesheet names it.
1454/// A slope at the head of a sentence.
1455fn sentence(slope: &str) -> String {
1456    let mut opens = slope.chars();
1457    match opens.next() {
1458        Some(first) => first.to_uppercase().collect::<String>() + opens.as_str(),
1459        None => String::new(),
1460    }
1461}
1462
1463fn slope(italic: bool) -> &'static str {
1464    if italic { "italic" } else { "upright" }
1465}
1466
1467/// A font stack as the sheet wrote it, for a diagnostic to quote.
1468fn stack(families: &[Family]) -> String {
1469    families
1470        .iter()
1471        .map(|family| match family {
1472            Family::Named(name) => name.clone(),
1473            Family::Generic(generic) => generic.keyword().to_string(),
1474        })
1475        .collect::<Vec<_>>()
1476        .join(", ")
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481    use super::*;
1482    use crate::content::{Attributes, Block, HeadingLevel, Inline, Metadata, Section, SourcePos};
1483    use crate::fonts::{
1484        BUNDLED_FONT, FaceAttributes, FeatureSetting, GenericFamily, bundled_registry,
1485    };
1486    use crate::lines::{HangEnd, HangingPunctuation};
1487
1488    fn registry() -> &'static FontRegistry {
1489        static REGISTRY: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
1490        REGISTRY.get_or_init(|| bundled_registry().expect("bundled font parses"))
1491    }
1492
1493    /// A sheet reads back as it was written: selectors as CSS, the
1494    /// declarations the parser kept with their positions, and the
1495    /// at-rules. What the parser refused is a warning and not here.
1496    #[test]
1497    fn a_sheet_reads_back_as_it_was_written() {
1498        let css = "@page :left { margin: 1in }\n\
1499                   h1 + p, NOTE::first-letter {\n  color: red;\n  float: left;\n  --x: 1pt;\n}\n\
1500                   @font-face { font-family: Author; src: url(a.otf), local(\"B\") }";
1501        let sheets = Stylesheets::parse(&[Source::author("a.css", css)]);
1502        let written = sheets.written();
1503        assert_eq!(written.len(), 2);
1504        assert_eq!(written[0].origin, Origin::UserAgent);
1505
1506        let author = &written[1];
1507        assert_eq!(author.origin, Origin::Author);
1508        assert_eq!(author.pages[0].selector, "@page :left");
1509        assert_eq!(author.pages[0].position.to_string(), "a.css:1:1");
1510        let rule = &author.rules[0];
1511        assert_eq!(rule.selectors, ["h1 + p", "NOTE::first-letter"]);
1512        assert_eq!(rule.position.to_string(), "a.css:2:1");
1513        let kept: Vec<(&str, &str, String)> = rule
1514            .declarations
1515            .iter()
1516            .map(|d| (&*d.property, &*d.value, d.position.to_string()))
1517            .collect();
1518        assert_eq!(
1519            kept,
1520            [
1521                ("color", "red", "a.css:3:3".to_string()),
1522                ("--x", "1pt", "a.css:5:3".to_string()),
1523            ]
1524        );
1525        assert!(
1526            sheets
1527                .warnings()
1528                .iter()
1529                .any(|w| w.origin.as_deref() == Some("a.css:4:3")),
1530            "{:?}",
1531            sheets.warnings()
1532        );
1533        assert_eq!(author.faces[0].family, "Author");
1534        assert_eq!(
1535            author.faces[0].src,
1536            [Src::Url("a.otf".into()), Src::Local("B".into())]
1537        );
1538    }
1539
1540    fn text(value: &str) -> Inline {
1541        Inline::Text {
1542            id: NodeId::UNASSIGNED,
1543            value: value.into(),
1544            attributes: Attributes::default(),
1545            position: None,
1546            span: None,
1547        }
1548    }
1549
1550    /// A chapter: a heading, a paragraph opening with emphasis, a
1551    /// paragraph of plain prose, and a blockquote.
1552    fn sample() -> Book {
1553        let mut book = Book {
1554            metadata: Metadata::default(),
1555            sections: vec![Section {
1556                attributes: Default::default(),
1557                id: NodeId::UNASSIGNED,
1558                source: Some("chapter-01.md".into()),
1559                title: None,
1560                blocks: vec![
1561                    Block::Heading {
1562                        id: NodeId::UNASSIGNED,
1563                        level: HeadingLevel::H1,
1564                        inlines: vec![text("Chapter One")],
1565                        attributes: Attributes::default(),
1566                        position: None,
1567                        span: None,
1568                    },
1569                    Block::Paragraph {
1570                        id: NodeId::UNASSIGNED,
1571                        inlines: vec![
1572                            Inline::Emphasis {
1573                                id: NodeId::UNASSIGNED,
1574                                children: vec![text("In which")],
1575                                attributes: Attributes::default(),
1576                                position: None,
1577                                span: None,
1578                            },
1579                            text(" a drawer is opened."),
1580                        ],
1581                        attributes: Attributes::default(),
1582                        position: None,
1583                        span: None,
1584                    },
1585                    Block::Paragraph {
1586                        id: NodeId::UNASSIGNED,
1587                        inlines: vec![text("It was the kind of morning.")],
1588                        attributes: Attributes::default(),
1589                        position: None,
1590                        span: None,
1591                    },
1592                    Block::Blockquote {
1593                        id: NodeId::UNASSIGNED,
1594                        blocks: vec![Block::Paragraph {
1595                            id: NodeId::UNASSIGNED,
1596                            inlines: vec![text("\"Nobody's early here.\"")],
1597                            attributes: Attributes::default(),
1598                            position: None,
1599                            span: None,
1600                        }],
1601                        attributes: Attributes::default(),
1602                        position: None,
1603                        span: None,
1604                    },
1605                ],
1606                position: None,
1607                span: None,
1608            }],
1609        };
1610        book.assign_node_ids();
1611        book
1612    }
1613
1614    /// The tree, with `css` cascading over the built-in sheet.
1615    fn compile(book: &Book, css: &str) -> StyleTree {
1616        Stylesheets::parse(&[Source::author("author.css", css)]).compile(book, registry())
1617    }
1618
1619    /// The computed style of the nth element with a given name.
1620    fn nth(tree: &StyleTree, element: &str, index: usize) -> ComputedStyle {
1621        let node = tree
1622            .nodes()
1623            .iter()
1624            .filter(|node| node.element == element)
1625            .nth(index)
1626            .unwrap_or_else(|| panic!("no {element} #{index}"));
1627        tree.styles()[node.style as usize].clone()
1628    }
1629
1630    fn first(tree: &StyleTree, element: &str) -> ComputedStyle {
1631        nth(tree, element, 0)
1632    }
1633
1634    /// Two chapters of the sample, the first named `front` and
1635    /// `preface`, the second `body`.
1636    fn named_sections() -> Book {
1637        let mut book = sample();
1638        let mut body = book.sections[0].clone();
1639        body.source = Some("chapter-02.md".into());
1640        body.attributes.classes = vec!["body".into()];
1641        book.sections[0].attributes = Attributes {
1642            id: Some("preface".into()),
1643            classes: vec!["front".into()],
1644        };
1645        book.sections.push(body);
1646        book.assign_node_ids();
1647        book
1648    }
1649
1650    /// Acceptance: the page-name and page-counter examples on the
1651    /// subset page reach the sections they name, and no other.
1652    #[test]
1653    fn the_subset_page_section_examples_match() {
1654        let tree = compile(
1655            &named_sections(),
1656            "section.front { page: front }
1657             section.body { counter-reset: page 1 }",
1658        );
1659        let (front, body) = (nth(&tree, "section", 0), nth(&tree, "section", 1));
1660        assert_eq!(front.page.as_deref(), Some("front"));
1661        assert_ne!(body.page.as_deref(), Some("front"));
1662        assert_eq!(body.counter_reset, Some(1));
1663        assert_eq!(front.counter_reset, None);
1664    }
1665
1666    /// Acceptance: a section's class and id match in the cascade, and
1667    /// `:is()` and a descendant selector see them.
1668    #[test]
1669    fn a_section_class_and_id_match_in_every_selector_form() {
1670        let tree = compile(
1671            &named_sections(),
1672            "section#preface { page: by-id }
1673             :is(section.body) { page: by-is }
1674             section.front p { page: below-front }
1675             :is(section.body) > h1 { page: below-body }",
1676        );
1677        assert_eq!(nth(&tree, "section", 0).page.as_deref(), Some("by-id"));
1678        assert_eq!(nth(&tree, "section", 1).page.as_deref(), Some("by-is"));
1679        // Each chapter of the sample holds three paragraphs.
1680        assert_eq!(nth(&tree, "p", 2).page.as_deref(), Some("below-front"));
1681        assert_ne!(nth(&tree, "p", 3).page.as_deref(), Some("below-front"));
1682        assert_ne!(nth(&tree, "h1", 0).page.as_deref(), Some("below-body"));
1683        assert_eq!(nth(&tree, "h1", 1).page.as_deref(), Some("below-body"));
1684    }
1685
1686    /// A book of two images, the first of them named.
1687    fn images() -> Book {
1688        let image = |named: Attributes| Block::Image {
1689            id: NodeId::UNASSIGNED,
1690            url: "plate.jpg".into(),
1691            alt: "a map".into(),
1692            attributes: named,
1693            position: None,
1694            span: None,
1695        };
1696        let mut book = Book {
1697            metadata: Metadata::default(),
1698            sections: vec![Section {
1699                attributes: Default::default(),
1700                id: NodeId::UNASSIGNED,
1701                source: Some("chapter-01.md".into()),
1702                title: None,
1703                blocks: vec![
1704                    image(Attributes {
1705                        id: Some("frontispiece".into()),
1706                        classes: vec!["map".into()],
1707                    }),
1708                    image(Attributes::default()),
1709                ],
1710                position: None,
1711                span: None,
1712            }],
1713        };
1714        book.assign_node_ids();
1715        book
1716    }
1717
1718    /// A table of a header row and three body rows, its second column
1719    /// written centered.
1720    fn table() -> Book {
1721        use crate::content::{Alignment, Cell, Row};
1722        let cell = |value: &str, align: Option<Alignment>| Cell {
1723            blocks: vec![Block::Paragraph {
1724                id: NodeId::UNASSIGNED,
1725                inlines: vec![text(value)],
1726                attributes: Attributes::default(),
1727                position: None,
1728                span: None,
1729            }],
1730            align,
1731            ..Cell::default()
1732        };
1733        let row = |left: &str, right: &str| Row {
1734            cells: vec![cell(left, None), cell(right, Some(Alignment::Center))],
1735            ..Row::default()
1736        };
1737        let mut book = Book {
1738            metadata: Metadata::default(),
1739            sections: vec![Section {
1740                attributes: Default::default(),
1741                id: NodeId::UNASSIGNED,
1742                source: Some("chapter-01.md".into()),
1743                title: None,
1744                blocks: vec![Block::Table {
1745                    id: NodeId::UNASSIGNED,
1746                    head: vec![row("Pocket", "Found")],
1747                    body: vec![
1748                        row("Right", "A handkerchief"),
1749                        row("Left", "A snuff-box"),
1750                        row("Fob", "A watch"),
1751                    ],
1752                    attributes: Attributes::default(),
1753                    position: None,
1754                    span: None,
1755                }],
1756                position: None,
1757                span: None,
1758            }],
1759        };
1760        book.assign_node_ids();
1761        book
1762    }
1763
1764    /// Acceptance: `z-index` is the layer a block paints in. It is a
1765    /// block's own: a child of a raised block is not raised with it,
1766    /// and `auto` is layer 0.
1767    #[test]
1768    fn a_block_takes_the_layer_it_names_and_a_child_takes_none_of_it() {
1769        let tree = compile(
1770            &sample(),
1771            "blockquote { z-index: 10 }
1772             h1 { z-index: -1 }
1773             p { z-index: auto }",
1774        );
1775        assert_eq!(first(&tree, "blockquote").z_index, 10);
1776        assert_eq!(first(&tree, "h1").z_index, -1);
1777        // The paragraph inside the blockquote is the last of them.
1778        assert_eq!(nth(&tree, "p", 2).z_index, 0);
1779        assert_eq!(first(&tree, "section").z_index, 0);
1780    }
1781
1782    /// Acceptance: `tbody tr:nth-child(odd)` counts the rows of the
1783    /// body, so it reaches the first and the third of them and not
1784    /// the header row.
1785    #[test]
1786    fn a_body_row_counts_among_the_rows_of_the_body() {
1787        let tint = Some(Color::rgb(0xe3, 0xe3, 0xe3));
1788        let tree = compile(
1789            &table(),
1790            "tbody tr:nth-child(odd) { background-color: #e3e3e3 }",
1791        );
1792        let rows: Vec<Option<Color>> = (0..4)
1793            .map(|index| nth(&tree, "tr", index).background.color)
1794            .collect();
1795        assert_eq!(rows, [None, tint, None, tint]);
1796        assert_eq!(first(&tree, "thead").background.color, None);
1797    }
1798
1799    /// Acceptance: the alignment the delimiter row wrote beats the
1800    /// built-in sheet and reaches the prose in the cell. Any rule of
1801    /// the author's beats it, even one with no specificity.
1802    #[test]
1803    fn the_alignment_a_table_wrote_sits_under_every_author_rule() {
1804        let book = table();
1805        let tree = defaults(&book, registry());
1806        assert_eq!(nth(&tree, "td", 0).text_align, TextAlign::Left);
1807        assert_eq!(nth(&tree, "td", 1).text_align, TextAlign::Center);
1808        assert_eq!(nth(&tree, "th", 1).text_align, TextAlign::Center);
1809        // The header row holds the first two paragraphs, so the first
1810        // body row's second cell holds the fourth.
1811        assert_eq!(nth(&tree, "p", 3).text_align, TextAlign::Center);
1812
1813        let tree = compile(&book, "td { text-align: right }");
1814        assert_eq!(nth(&tree, "td", 1).text_align, TextAlign::Right);
1815        assert_eq!(nth(&tree, "th", 1).text_align, TextAlign::Center);
1816
1817        let tree = compile(&book, "* { text-align: justify }");
1818        assert_eq!(nth(&tree, "td", 1).text_align, TextAlign::Justify);
1819    }
1820
1821    /// `width` computes to points, or stays a percentage of a table
1822    /// the cascade has not sized. `border-collapse` inherits, so the
1823    /// cells of a collapsed table are collapsed too.
1824    #[test]
1825    fn width_and_border_collapse_compute() {
1826        let book = table();
1827        let tree = compile(&book, "th:first-of-type { width: 8em }");
1828        let header = first(&tree, "th");
1829        assert_eq!(header.width, Width::Points(8.0 * header.font_size));
1830        assert_eq!(nth(&tree, "th", 1).width, Width::Auto);
1831        assert_eq!(
1832            first(&tree, "td").width,
1833            Width::Auto,
1834            "width does not inherit"
1835        );
1836
1837        let tree = compile(&book, "th { width: 25% }");
1838        assert_eq!(first(&tree, "th").width, Width::Percent(25.0));
1839
1840        let tree = defaults(&book, registry());
1841        assert_eq!(
1842            first(&tree, "table").border_collapse,
1843            BorderCollapse::Collapse
1844        );
1845        assert_eq!(first(&tree, "td").border_collapse, BorderCollapse::Collapse);
1846        let tree = compile(&book, "table { border-collapse: separate }");
1847        assert_eq!(first(&tree, "td").border_collapse, BorderCollapse::Separate);
1848    }
1849
1850    /// `column-span` is `none` until a sheet says otherwise, and it
1851    /// does not inherit: the cells of a spanning table are set in
1852    /// the table, not across the page a second time.
1853    #[test]
1854    fn column_span_computes_and_does_not_inherit() {
1855        let book = table();
1856        let tree = defaults(&book, registry());
1857        assert_eq!(first(&tree, "table").column_span, ColumnSpan::None);
1858
1859        let tree = compile(&book, "table { column-span: all }");
1860        assert_eq!(first(&tree, "table").column_span, ColumnSpan::All);
1861        assert_eq!(first(&tree, "td").column_span, ColumnSpan::None);
1862        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
1863
1864        let tree = compile(
1865            &book,
1866            "table { column-span: all } table { column-span: none }",
1867        );
1868        assert_eq!(first(&tree, "table").column_span, ColumnSpan::None);
1869    }
1870
1871    /// A class names one element out of a kind of them: the image
1872    /// that carries it is set, and the image beside it is not.
1873    #[test]
1874    fn a_class_reaches_the_element_that_carries_it_and_no_other() {
1875        let book = images();
1876        let tree = compile(&book, ".map { text-align: right }");
1877        assert_eq!(nth(&tree, "img", 0).text_align, TextAlign::Right);
1878        assert_eq!(nth(&tree, "img", 1).text_align, TextAlign::Left);
1879    }
1880
1881    /// An id names one element and nothing else, and outranks the
1882    /// class on the same element.
1883    #[test]
1884    fn an_id_reaches_the_element_that_carries_it_and_outranks_a_class() {
1885        let book = images();
1886        let tree = compile(
1887            &book,
1888            ".map { break-before: page } #frontispiece { break-before: recto }",
1889        );
1890        assert_eq!(nth(&tree, "img", 0).break_before, Break::Side(Side::Recto));
1891        assert_eq!(nth(&tree, "img", 1).break_before, Break::Auto);
1892    }
1893
1894    /// An id names one element, so a second element under it is a
1895    /// mistake. Both still match: the sheet reaches two, the origin
1896    /// is the second, and the message names the first.
1897    #[test]
1898    fn an_id_written_twice_warns_naming_both_places() {
1899        let mut book = images();
1900        let Block::Image {
1901            attributes,
1902            position,
1903            ..
1904        } = &mut book.sections[0].blocks[1]
1905        else {
1906            panic!("expected an image");
1907        };
1908        attributes.id = Some("frontispiece".into());
1909        *position = Some(SourcePos {
1910            line: 12,
1911            column: 1,
1912        });
1913        let Block::Image { position, .. } = &mut book.sections[0].blocks[0] else {
1914            panic!("expected an image");
1915        };
1916        *position = Some(SourcePos { line: 3, column: 1 });
1917
1918        let tree = compile(&book, "#frontispiece { break-before: recto }");
1919        assert_eq!(nth(&tree, "img", 0).break_before, Break::Side(Side::Recto));
1920        assert_eq!(nth(&tree, "img", 1).break_before, Break::Side(Side::Recto));
1921        let repeated: Vec<&Warning> = tree
1922            .warnings()
1923            .iter()
1924            .filter(|warning| warning.message.contains("frontispiece"))
1925            .collect();
1926        assert_eq!(repeated.len(), 1, "{:?}", tree.warnings());
1927        assert_eq!(
1928            repeated[0].message,
1929            "Id `frontispiece` is duplicated. Applied at chapter-01.md:3:1.",
1930        );
1931        assert_eq!(repeated[0].origin.as_deref(), Some("chapter-01.md:12:1"));
1932    }
1933
1934    /// The built-in sheet sets the body, chapter and folio styles, and
1935    /// the trade-paperback page.
1936    #[test]
1937    fn the_built_in_sheet_sets_the_defaults() {
1938        let book = sample();
1939        let tree = defaults(&book, registry());
1940
1941        let body = first(&tree, "p");
1942        assert_eq!(body.font_size, 11.0);
1943        assert_eq!(body.line_height, 1.4);
1944        assert_eq!(body.font_id, 0);
1945        assert_eq!(body.hyphens, Hyphens::None);
1946        assert_eq!(first(&tree, "h1").font_size, 18.0);
1947
1948        let recto = tree.page(PageQuery {
1949            name: Some("chapter"),
1950            situation: Situation::Body(Side::Recto),
1951        });
1952        assert_eq!(recto.geometry.width, 432.0);
1953        assert_eq!(recto.geometry.height, 648.0);
1954        assert_eq!(recto.geometry.content_size(), (336.0, 540.0));
1955        let folio = recto
1956            .margin_box(MarginBox::BottomCenter)
1957            .expect("the folio is a margin box");
1958        assert_eq!(folio.content, Content::Counter(CounterStyle::Decimal));
1959        assert_eq!(folio.style.font_size, 9.0);
1960        assert_eq!(folio.style.line_height, 1.4);
1961    }
1962
1963    /// The four background properties compute on a block: a url the
1964    /// sheet wrote, and how it is drawn.
1965    #[test]
1966    fn the_background_properties_compute_on_a_block() {
1967        let tree = compile(
1968            &sample(),
1969            "p {\n  background-color: #f4f1ea;\n  background-image: url(\"scan.webp\");\n  \
1970             background-repeat: no-repeat;\n  background-size: cover;\n  \
1971             background-position: right bottom;\n}",
1972        );
1973        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
1974        let background = first(&tree, "p").background;
1975        assert_eq!(background.color, Some(Color::rgb(0xf4, 0xf1, 0xea)));
1976        assert_eq!(
1977            background.image.as_ref().map(|url| url.value.as_str()),
1978            Some("scan.webp"),
1979        );
1980        assert_eq!(background.repeat, BackgroundRepeat::NoRepeat);
1981        assert_eq!(background.size, BackgroundSize::Cover);
1982        assert_eq!(background.position.x, Coord::Percent(100.0));
1983        assert_eq!(background.position.y, Coord::Percent(100.0));
1984        assert!(!first(&tree, "blockquote").background.paints());
1985    }
1986
1987    /// A url carries where it was written, so a warning about it can
1988    /// name the line and column long after the sheet was parsed.
1989    #[test]
1990    fn a_background_url_carries_where_the_sheet_wrote_it() {
1991        let tree = compile(&sample(), "p {\n  background-image: url(scan.webp);\n}");
1992        let url = first(&tree, "p")
1993            .background
1994            .image
1995            .expect("the url reached the style");
1996        assert_eq!(url.origin.as_deref(), Some("author.css:2:3"));
1997    }
1998
1999    /// `background-size` and `background-position` take their length
2000    /// forms, and the cascade resolves `em` against the font size in
2001    /// force while a percentage stays one.
2002    #[test]
2003    fn background_lengths_compute_and_percentages_stay() {
2004        let tree = compile(
2005            &sample(),
2006            "p {\n  font-size: 10pt;\n  background-size: 2em 50%;\n  \
2007             background-position: 1em 25%;\n}",
2008        );
2009        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
2010        let background = first(&tree, "p").background;
2011        assert_eq!(
2012            background.size,
2013            BackgroundSize::Fixed {
2014                width: Some(Coord::Points(20.0)),
2015                height: Some(Coord::Percent(50.0)),
2016            },
2017        );
2018        assert_eq!(background.position.x, Coord::Points(10.0));
2019        assert_eq!(background.position.y, Coord::Percent(25.0));
2020    }
2021
2022    /// A background does not inherit: a rule on the quotation leaves
2023    /// the paragraph inside it painting nothing.
2024    #[test]
2025    fn a_background_does_not_reach_the_children_of_the_box() {
2026        let tree = compile(&sample(), "blockquote { background-image: url(scan.webp) }");
2027        assert!(first(&tree, "blockquote").background.paints());
2028        assert!(!first(&tree, "p").background.paints());
2029    }
2030
2031    /// Each side of the spread takes its own scan, and `@page` reads
2032    /// the same four properties a block does.
2033    #[test]
2034    fn each_side_of_the_spread_takes_its_own_background() {
2035        let tree = compile(
2036            &sample(),
2037            "@page :left { background-image: url(verso.webp); background-size: cover }\n\
2038             @page :right { background-image: url(recto.webp); background-size: contain }",
2039        );
2040        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
2041        let named = |side| {
2042            tree.page(PageQuery {
2043                name: Some("chapter"),
2044                situation: Situation::Body(side),
2045            })
2046            .background
2047            .clone()
2048        };
2049        let verso = named(Side::Verso);
2050        assert_eq!(
2051            verso.image.as_ref().map(|url| url.value.as_str()),
2052            Some("verso.webp"),
2053        );
2054        assert_eq!(verso.size, BackgroundSize::Cover);
2055        let recto = named(Side::Recto);
2056        assert_eq!(
2057            recto.image.as_ref().map(|url| url.value.as_str()),
2058            Some("recto.webp"),
2059        );
2060        assert_eq!(recto.size, BackgroundSize::Contain);
2061    }
2062
2063    /// Part: `align-content` on `@page` reaches the page box of the
2064    /// pages the rule selects, and a page that names nothing keeps its
2065    /// content at the top.
2066    #[test]
2067    fn align_content_reaches_the_page_box() {
2068        use crate::style::AlignContent;
2069        let book = sample();
2070        let css = "@page chapter:first { align-content: center } \
2071                   @page :left { align-content: end }";
2072        let align = |css: &str, situation: Situation| {
2073            compile(&book, css)
2074                .page(PageQuery {
2075                    name: Some("chapter"),
2076                    situation,
2077                })
2078                .geometry
2079                .align_content
2080        };
2081        assert_eq!(align("", Situation::Body(Side::Recto)), AlignContent::Start);
2082        assert_eq!(
2083            align(css, Situation::First(Side::Recto)),
2084            AlignContent::Center
2085        );
2086        assert_eq!(align(css, Situation::Body(Side::Verso)), AlignContent::End);
2087        assert_eq!(
2088            align(css, Situation::Body(Side::Recto)),
2089            AlignContent::Start
2090        );
2091    }
2092
2093    /// The column properties resolve to points against the page's
2094    /// content box: a count divides it, a width takes as many columns
2095    /// of that width as fit, the two together take the smaller
2096    /// answer, and `normal` is one em of the book's own size.
2097    #[test]
2098    fn columns_divide_the_content_box() {
2099        let book = sample();
2100        let page = |css: &str| {
2101            compile(&book, css)
2102                .page(PageQuery {
2103                    name: Some("chapter"),
2104                    situation: Situation::Body(Side::Recto),
2105                })
2106                .geometry
2107        };
2108
2109        let plain = page("");
2110        assert_eq!(plain.column_count(), 1);
2111        assert_eq!(plain.measure(), 336.0);
2112        assert_eq!(plain.columns.gap, compile(&book, "").root().font_size);
2113
2114        let two = page("@page { column-count: 2; column-gap: 16pt }");
2115        assert_eq!(two.column_count(), 2);
2116        assert_eq!(two.measure(), 160.0);
2117        assert_eq!(two.column_origin(0), (54.0, 54.0));
2118        assert_eq!(two.column_origin(1), (230.0, 54.0));
2119
2120        // 336 points of content box takes three 100pt columns with
2121        // 16pt gutters between them, and the columns widen to fill
2122        // what is left over.
2123        let by_width = page("@page { column-width: 100pt; column-gap: 16pt }");
2124        assert_eq!(by_width.column_count(), 3);
2125        assert_eq!(by_width.measure(), (336.0 - 32.0) / 3.0);
2126
2127        // A count declared beside a width is the ceiling on it.
2128        let both = page("@page { column-count: 2; column-width: 100pt; column-gap: 16pt }");
2129        assert_eq!(both.column_count(), 2);
2130
2131        // A width nothing fits still leaves one column.
2132        assert_eq!(page("@page { column-width: 900pt }").column_count(), 1);
2133
2134        let ruled = page("@page { column-count: 2; column-rule-style: solid }");
2135        assert_eq!(ruled.columns.rule.used(), crate::style::Border::NONE.width);
2136        assert_eq!(
2137            page("@page { column-rule-width: thin }")
2138                .columns
2139                .rule
2140                .used(),
2141            0.0,
2142            "a rule with no style is not drawn",
2143        );
2144    }
2145
2146    /// Selectors run against the content tree: type, descendant,
2147    /// child, `:first-child` and `:is()` all pick out the elements a
2148    /// reader of the markdown would expect.
2149    #[test]
2150    fn selectors_match_the_content_tree() {
2151        let book = sample();
2152        let tree = compile(
2153            &book,
2154            "section > p { text-indent: 12pt }
2155             blockquote p { text-indent: 3pt }
2156             :is(h1, h2):first-child { text-indent: 6pt }
2157             :is(em, strong) { font-weight: 700 }",
2158        );
2159        // The section's own paragraphs are indented; the one inside
2160        // the blockquote is not a child of the section, so only the
2161        // descendant rule reaches it.
2162        assert_eq!(nth(&tree, "p", 0).text_indent, 12.0);
2163        assert_eq!(nth(&tree, "p", 1).text_indent, 12.0);
2164        assert_eq!(nth(&tree, "p", 2).text_indent, 3.0);
2165        // The heading opens the section, so it is a first child.
2166        assert_eq!(first(&tree, "h1").text_indent, 6.0);
2167        assert_eq!(first(&tree, "em").font_weight, 700);
2168
2169        // Siblings, negation, counting and `:has()` all reach the
2170        // same tree.
2171        let tree = compile(
2172            &book,
2173            "h1 + p { text-indent: 1pt }
2174             h1 ~ p { widows: 3 }
2175             p:not(:first-child) { orphans: 4 }
2176             section:has(blockquote) > p:nth-child(3) { text-indent: 2pt }",
2177        );
2178        assert_eq!(nth(&tree, "p", 0).text_indent, 1.0);
2179        assert_eq!(nth(&tree, "p", 1).text_indent, 2.0);
2180        assert_eq!(nth(&tree, "p", 0).widows, 3);
2181        assert_eq!(nth(&tree, "p", 0).orphans, 4);
2182        assert_eq!(first(&tree, "h1").orphans, 2);
2183    }
2184
2185    /// Author CSS overrides the built-in sheet, and specificity and
2186    /// source order decide between author rules.
2187    #[test]
2188    fn author_css_overrides_the_built_in_sheet() {
2189        let book = sample();
2190        // The built-in sheet's `book { font-size: 11pt }` is more
2191        // specific than nothing, and still loses to the author.
2192        let tree = compile(&book, "* { font-size: 13pt } p { font-size: 12pt }");
2193        assert_eq!(first(&tree, "p").font_size, 12.0);
2194        // Equal specificity: the later rule wins.
2195        let tree = compile(&book, "p { font-size: 12pt } p { font-size: 14pt }");
2196        assert_eq!(first(&tree, "p").font_size, 14.0);
2197        // `!important` in the built-in sheet would outrank the
2198        // author, and nothing in it uses one.
2199        let tree = compile(
2200            &book,
2201            "p { font-size: 12pt !important } p { font-size: 14pt }",
2202        );
2203        assert_eq!(first(&tree, "p").font_size, 12.0);
2204    }
2205
2206    /// An unsupported property is a diagnostic naming its source
2207    /// position, and the rest of the rule still applies.
2208    #[test]
2209    fn unsupported_properties_warn_and_the_run_continues() {
2210        let book = sample();
2211        let tree = compile(
2212            &book,
2213            "p {\n  text-shadow: 0 0 2px black;\n  font-size: 13pt;\n}\n",
2214        );
2215        let warning = tree
2216            .warnings()
2217            .iter()
2218            .find(|warning| warning.message.contains("text-shadow"))
2219            .expect("text-shadow is outside the subset");
2220        assert_eq!(
2221            warning.message,
2222            "Unsupported property `text-shadow`. The declaration is ignored."
2223        );
2224        assert_eq!(warning.origin.as_deref(), Some("author.css:2:3"));
2225        assert_eq!(
2226            first(&tree, "p").font_size,
2227            13.0,
2228            "the declaration after the bad one was dropped"
2229        );
2230    }
2231
2232    /// A sheet that breaks the subset every way it can. Each warning
2233    /// is a sentence, and says what happened to the declaration or to
2234    /// the rule.
2235    #[test]
2236    fn every_css_warning_names_what_was_ignored() {
2237        let css = "p { text-shadow: 0 0 2px black }\n\
2238                   p { text-align: bananas }\n\
2239                   p::first-line { font-family: serif }\n\
2240                   @media print { p { color: red } }\n\
2241                   @page :nth(2) { size: a4 }\n\
2242                   p:hover { color: red }\n\
2243                   p { color: }\n";
2244        let sheets = Stylesheets::parse(&[Source::author("author.css", css)]);
2245        let warnings = sheets.warnings();
2246        assert!(warnings.len() >= 6, "{warnings:?}");
2247        for warning in warnings {
2248            let message = &warning.message;
2249            assert!(
2250                message.starts_with(|opens: char| opens.is_uppercase()),
2251                "{message}",
2252            );
2253            assert!(!message.contains(';'), "{message}");
2254            assert!(
2255                message.ends_with("The declaration is ignored.")
2256                    || message.ends_with("The rule is ignored."),
2257                "{message}",
2258            );
2259        }
2260    }
2261
2262    /// A font file that will not parse and a font with no family name
2263    /// are different mistakes, so they read apart.
2264    #[test]
2265    fn the_two_font_face_failures_read_apart() {
2266        assert_eq!(
2267            face_failure("Sabon", &FontError::Parse),
2268            "Sabon could not be read as a font. Falling back to another registered font.",
2269        );
2270        assert_eq!(
2271            face_failure("Sabon", &FontError::MissingName),
2272            "Sabon has no family name and was not registered. \
2273             Falling back to another registered font.",
2274        );
2275    }
2276
2277    /// The rest of the subset's edges: an at-rule, a selector and a
2278    /// value the engine does not know each warn where they were
2279    /// written, and the sheet keeps parsing.
2280    #[test]
2281    fn unsupported_at_rules_selectors_and_values_warn() {
2282        let book = sample();
2283        let tree = compile(
2284            &book,
2285            "@media print { p { font-size: 30pt } }\n\
2286             p:hover { font-size: 40pt }\n\
2287             p { text-align: sideways }\n\
2288             p { hanging-punctuation: sideways }\n\
2289             p { font-size: 15pt }\n",
2290        );
2291        let messages: Vec<&str> = tree
2292            .warnings()
2293            .iter()
2294            .map(|warning| warning.message.as_str())
2295            .collect();
2296        assert!(
2297            messages.contains(&"Unsupported at-rule `@media`. The rule is ignored."),
2298            "{messages:?}"
2299        );
2300        assert!(
2301            messages.contains(&"Unsupported selector `:hover`. The rule is ignored."),
2302            "{messages:?}"
2303        );
2304        assert!(
2305            messages.contains(&"Unsupported value for `text-align`. The declaration is ignored."),
2306            "{messages:?}"
2307        );
2308        assert!(
2309            messages.contains(
2310                &"Unsupported value for `hanging-punctuation`. The declaration is ignored."
2311            ),
2312            "{messages:?}"
2313        );
2314        assert!(
2315            tree.warnings()
2316                .iter()
2317                .all(|warning| warning.origin.is_some()),
2318            "a diagnostic with no position: {:?}",
2319            tree.warnings()
2320        );
2321        assert_eq!(first(&tree, "p").font_size, 15.0);
2322    }
2323
2324    /// `@page :first` and `:left`/`:right` select different masters
2325    /// for the same book: a chapter opening runs a blind folio, and
2326    /// the spread's margins mirror.
2327    #[test]
2328    fn page_pseudo_classes_select_different_masters() {
2329        let book = sample();
2330        let tree = defaults(&book, registry());
2331        let chapter = |situation| {
2332            tree.page(PageQuery {
2333                name: Some("chapter"),
2334                situation,
2335            })
2336        };
2337        let opening = chapter(Situation::First(Side::Recto));
2338        let body = chapter(Situation::Body(Side::Recto));
2339        let verso = chapter(Situation::Body(Side::Verso));
2340        let blank = chapter(Situation::Blank);
2341
2342        assert!(opening.margin_box(MarginBox::BottomCenter).is_none());
2343        // A blank's master is a master like any other; that nothing
2344        // paints on a blank is the flow's rule, not the sheet's.
2345        assert_eq!(blank.geometry, verso.geometry);
2346        assert_eq!(
2347            body.margin_box(MarginBox::BottomCenter)
2348                .map(|box_| &box_.content),
2349            Some(&Content::Counter(CounterStyle::Decimal))
2350        );
2351        assert_eq!(body.geometry.margin.left, 54.0);
2352        assert_eq!(body.geometry.margin.right, 42.0);
2353        assert_eq!(verso.geometry.margin.left, 42.0);
2354        assert_eq!(verso.geometry.margin.right, 54.0);
2355        // Every situation of every named page has a master.
2356        assert_eq!(tree.masters().len(), 10);
2357    }
2358
2359    /// Part: the `text-decoration` shorthand sets all four
2360    /// longhands, and puts back to its initial value every one it
2361    /// leaves out.
2362    #[test]
2363    fn the_text_decoration_shorthand_sets_every_longhand() {
2364        let book = sample();
2365        let tree = compile(
2366            &book,
2367            "p { text-decoration: underline double #808080 1.5pt }
2368             em { text-decoration: line-through }",
2369        );
2370        let paragraph = first(&tree, "p");
2371        assert_eq!(
2372            paragraph.text_decoration_line,
2373            DecorationLine {
2374                under: true,
2375                over: false,
2376                through: false,
2377            },
2378        );
2379        assert_eq!(paragraph.text_decoration_style, DecorationStyle::Double);
2380        assert_eq!(
2381            paragraph.text_decoration_color,
2382            Some(Color::rgb(128, 128, 128)),
2383        );
2384        assert_eq!(paragraph.text_decoration_thickness, Some(1.5));
2385
2386        // The emphasis is inside the paragraph, so it inherits all
2387        // four and then the shorthand puts back the three it does
2388        // not name.
2389        let emphasis = first(&tree, "em");
2390        assert_eq!(
2391            emphasis.text_decoration_line,
2392            DecorationLine {
2393                under: false,
2394                over: false,
2395                through: true,
2396            },
2397        );
2398        assert_eq!(emphasis.text_decoration_style, DecorationStyle::Solid);
2399        assert_eq!(emphasis.text_decoration_color, None);
2400        assert_eq!(emphasis.text_decoration_thickness, None);
2401
2402        // The longhands inherit on their own: an emphasis the
2403        // shorthand does not reach keeps what the paragraph drew.
2404        let inherited = compile(&book, "p { text-decoration: underline }");
2405        assert_eq!(
2406            first(&inherited, "em").text_decoration_line,
2407            first(&inherited, "p").text_decoration_line,
2408        );
2409    }
2410
2411    /// The furniture grammar: what an element sets a running string
2412    /// to, where the folio restarts, and what a margin box resolves
2413    /// its content from.
2414    #[test]
2415    fn string_set_counter_reset_and_margin_box_content_compile() {
2416        let book = sample();
2417        let tree = compile(
2418            &book,
2419            "h1 { string-set: chapter \"— \" content() }
2420             section { counter-reset: page 7 }
2421             @page :left { @top-left { content: string(chapter) } }
2422             @page :right { @top-right { content: counter(page, upper-roman) } }",
2423        );
2424
2425        assert_eq!(
2426            first(&tree, "h1").string_set,
2427            vec![StringSet {
2428                name: "chapter".into(),
2429                value: vec![StringPiece::Text("— ".into()), StringPiece::Content],
2430            }],
2431        );
2432        // `content(text)` is the other spelling of the same thing,
2433        // and an argument the engine has no answer for is not.
2434        let spelled = compile(
2435            &book,
2436            "h1 { string-set: chapter content(text) }
2437             p { string-set: stanza content(before) }",
2438        );
2439        assert_eq!(
2440            first(&spelled, "h1").string_set,
2441            vec![StringSet {
2442                name: "chapter".into(),
2443                value: vec![StringPiece::Content],
2444            }],
2445        );
2446        assert!(first(&spelled, "p").string_set.is_empty());
2447        assert_eq!(spelled.warnings().len(), 1, "{:?}", spelled.warnings());
2448
2449        assert_eq!(first(&tree, "section").counter_reset, Some(7));
2450        // Not inherited: a paragraph inside the section restarts
2451        // nothing and sets nothing.
2452        assert_eq!(first(&tree, "p").counter_reset, None);
2453        assert!(first(&tree, "p").string_set.is_empty());
2454
2455        let box_content = |situation, which| {
2456            tree.page(PageQuery {
2457                name: Some("chapter"),
2458                situation,
2459            })
2460            .margin_box(which)
2461            .map(|box_| box_.content.clone())
2462        };
2463        assert_eq!(
2464            box_content(Situation::Body(Side::Verso), MarginBox::TopLeft),
2465            Some(Content::String("chapter".into())),
2466        );
2467        assert_eq!(
2468            box_content(Situation::Body(Side::Recto), MarginBox::TopRight),
2469            Some(Content::Counter(CounterStyle::UpperRoman)),
2470        );
2471    }
2472
2473    /// A counter style outside the subset is a diagnostic, not a
2474    /// silently decimal folio.
2475    #[test]
2476    fn an_unsupported_counter_style_warns() {
2477        let book = sample();
2478        let tree = compile(
2479            &book,
2480            "@page { @bottom-center { content: counter(page, georgian) } }",
2481        );
2482        assert!(
2483            tree.warnings().iter().any(|warning| warning.message
2484                == "Unsupported value for `content`. The declaration is ignored."),
2485            "{:?}",
2486            tree.warnings(),
2487        );
2488    }
2489
2490    /// The `@page` grammar: named pages, sheet sizes, margins, and a
2491    /// margin box set to a literal.
2492    #[test]
2493    fn page_grammar_sets_size_margins_and_margin_boxes() {
2494        let book = sample();
2495        let tree = compile(
2496            &book,
2497            "section { page: chapter }
2498             @page { size: a5; margin: 2cm }
2499             @page chapter { margin-left: 3cm }
2500             @page chapter:first { @top-center { content: \"❦\"; font-size: 14pt } }",
2501        );
2502        let a5 = (148.0 * 72.0 / 25.4, 210.0 * 72.0 / 25.4);
2503        let body = tree.page(PageQuery {
2504            name: Some("chapter"),
2505            situation: Situation::Body(Side::Recto),
2506        });
2507        assert!((body.geometry.width - a5.0).abs() < 1e-3);
2508        assert!((body.geometry.height - a5.1).abs() < 1e-3);
2509        assert!((body.geometry.margin.top - 2.0 * 72.0 / 2.54).abs() < 1e-3);
2510        assert!((body.geometry.margin.left - 3.0 * 72.0 / 2.54).abs() < 1e-3);
2511
2512        let opening = tree.page(PageQuery {
2513            name: Some("chapter"),
2514            situation: Situation::First(Side::Recto),
2515        });
2516        let ornament = opening
2517            .margin_box(MarginBox::TopCenter)
2518            .expect("the opening page has an ornament");
2519        assert_eq!(ornament.content, Content::Text("❦".into()));
2520        assert_eq!(ornament.style.font_size, 14.0);
2521        // An unnamed page never picked up the named rule's margin.
2522        let unnamed = tree.page(PageQuery {
2523            name: None,
2524            situation: Situation::Body(Side::Recto),
2525        });
2526        assert!((unnamed.geometry.margin.left - 2.0 * 72.0 / 2.54).abs() < 1e-3);
2527    }
2528
2529    /// `@font-face` resolves its `src` through the host loader, and
2530    /// the family the sheet declared is the one selectors use.
2531    #[test]
2532    fn font_face_src_resolves_through_the_host_loader() {
2533        struct OneFace;
2534        impl FontLoader for OneFace {
2535            fn load(&self, url: &str) -> Option<Vec<u8>> {
2536                (url == "faces/garamond.ttf").then(|| BUNDLED_FONT.to_vec())
2537            }
2538        }
2539
2540        let book = sample();
2541        let mut registry = bundled_registry().expect("bundled font parses");
2542        let css = "@font-face {
2543                     font-family: \"Author Serif\";
2544                     src: url(\"faces/garamond.ttf\") format(\"truetype\");
2545                   }
2546                   @font-face {
2547                     font-family: \"Missing\";
2548                     src: url(\"faces/nowhere.ttf\");
2549                   }
2550                   p { font-family: \"Author Serif\", serif }
2551                   h1 { font-family: \"Missing\", serif }";
2552        let mut sheets = Stylesheets::parse(&[Source::author("author.css", css)]);
2553        sheets.load_fonts(&mut registry, &OneFace);
2554        let tree = sheets.compile(&book, &registry);
2555
2556        let author = registry
2557            .by_family("author serif")
2558            .expect("the loaded face joined the registry");
2559        assert_eq!(registry.len(), bundled_registry().unwrap().len() + 5);
2560        assert_eq!(first(&tree, "p").font_id, author);
2561        // A face nothing resolved falls back to the next family.
2562        assert_eq!(first(&tree, "h1").font_id, 0);
2563        assert!(
2564            tree.warnings()
2565                .iter()
2566                .any(|warning| warning.message.contains("Missing")),
2567            "an unresolved face should say so: {:?}",
2568            tree.warnings()
2569        );
2570    }
2571
2572    /// The engine opens nothing itself: with a loader that resolves
2573    /// nothing, `@font-face` adds no face and says why.
2574    #[test]
2575    fn without_a_loader_no_font_face_resolves() {
2576        let book = sample();
2577        let mut registry = bundled_registry().expect("bundled font parses");
2578        let css = "@font-face { font-family: \"Author Serif\"; src: url(\"anywhere.ttf\") }";
2579        let mut sheets = Stylesheets::parse(&[Source::author("author.css", css)]);
2580        sheets.load_fonts(&mut registry, &NoFonts);
2581        let tree = sheets.compile(&book, &registry);
2582        assert_eq!(registry.len(), bundled_registry().unwrap().len());
2583        assert!(
2584            tree.warnings()
2585                .iter()
2586                .any(|w| w.message.contains("No src url loaded"))
2587        );
2588    }
2589
2590    /// Relative lengths compute against what CSS says they do:
2591    /// `em` in `font-size` against the parent, `rem` against the
2592    /// root, `em` elsewhere against the element's own size.
2593    #[test]
2594    fn relative_lengths_resolve_against_their_reference() {
2595        let book = sample();
2596        let tree = compile(
2597            &book,
2598            "book { font-size: 10pt }
2599             p { font-size: 1.2em; text-indent: 2em; margin-left: 1rem }
2600             em { font-size: 50% }",
2601        );
2602        let paragraph = first(&tree, "p");
2603        assert!((paragraph.font_size - 12.0).abs() < 1e-4);
2604        assert!((paragraph.text_indent - 24.0).abs() < 1e-4);
2605        assert!((paragraph.margin.left - 10.0).abs() < 1e-4);
2606        assert!((first(&tree, "em").font_size - 6.0).abs() < 1e-4);
2607    }
2608
2609    /// Line height computes to a multiple whatever it was written as.
2610    #[test]
2611    fn line_height_computes_to_a_multiple() {
2612        let book = sample();
2613        let tree = compile(
2614            &book,
2615            "p { font-size: 10pt; line-height: 15pt }
2616             h1 { line-height: 150% }
2617             em { line-height: normal }",
2618        );
2619        assert!((first(&tree, "p").line_height - 1.5).abs() < 1e-4);
2620        assert!((first(&tree, "h1").line_height - 1.5).abs() < 1e-4);
2621        assert!((first(&tree, "em").line_height - 1.2).abs() < 1e-4);
2622    }
2623
2624    /// Inheritance: text properties reach descendants, box properties
2625    /// do not.
2626    #[test]
2627    fn text_properties_inherit_and_box_properties_do_not() {
2628        let book = sample();
2629        let tree = compile(
2630            &book,
2631            "section { text-align: justify; margin-left: 20pt; hyphens: auto }",
2632        );
2633        let paragraph = first(&tree, "p");
2634        assert_eq!(paragraph.text_align, TextAlign::Justify);
2635        assert_eq!(paragraph.hyphens, Hyphens::Auto);
2636        assert_eq!(paragraph.margin.left, 0.0);
2637        assert_eq!(first(&tree, "section").margin.left, 20.0);
2638    }
2639
2640    /// The `inherited` flag on each row of the property table is what
2641    /// the cascade does: set on `section`, an inherited property
2642    /// reaches the paragraph under it and a box property does not.
2643    #[test]
2644    fn the_property_table_says_what_inherits() {
2645        let book = sample();
2646        let baseline = first(&compile(&book, ""), "p");
2647        for spec in sheet::PROPERTIES {
2648            let reached = spec.examples.iter().any(|example| {
2649                let css = format!("section {{ {}: {example} }}", spec.name);
2650                first(&compile(&book, &css), "p") != baseline
2651            });
2652            assert_eq!(reached, spec.inherited, "{}", spec.name);
2653        }
2654    }
2655
2656    /// The border shorthands compute onto the four edges: `border`
2657    /// sets all of them, a per-edge longhand one, and the per-value
2658    /// longhands one component of each. What a shorthand leaves out
2659    /// goes back to its initial value, so `border: solid` after a
2660    /// colour drops the colour.
2661    #[test]
2662    fn the_border_shorthands_reach_the_four_edges() {
2663        let book = sample();
2664        let tree = compile(
2665            &book,
2666            "p { border: 2pt solid crimson; border-left: 6pt solid }\n             blockquote { border-style: solid none; border-width: 1pt; border-color: teal }",
2667        );
2668        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
2669        let paragraph = first(&tree, "p");
2670        assert_eq!(paragraph.border.top.width, 2.0);
2671        assert_eq!(paragraph.border.top.style, BorderStyle::Solid);
2672        assert_eq!(paragraph.border.top.color, Some(Color::rgb(220, 20, 60)));
2673        assert_eq!(paragraph.border.left.width, 6.0);
2674        assert_eq!(paragraph.border.left.color, None);
2675        assert_eq!(paragraph.border.widths().inline(), 8.0);
2676
2677        let quote = first(&tree, "blockquote");
2678        assert_eq!(quote.border.widths().top, 1.0);
2679        assert_eq!(quote.border.widths().right, 0.0);
2680        assert_eq!(quote.border.left.color, Some(Color::rgb(0, 128, 128)));
2681    }
2682
2683    /// An edge `border-style` leaves at `none` takes no width in the
2684    /// flow, whatever `border-width` said, and one written with no
2685    /// width at all is `medium`.
2686    #[test]
2687    fn an_undrawn_edge_takes_no_width() {
2688        let book = sample();
2689        let tree = compile(
2690            &book,
2691            "p { border-width: 4pt }\nblockquote { border: solid }",
2692        );
2693        assert_eq!(first(&tree, "p").border.widths(), Edges::all(0.0));
2694        assert_eq!(first(&tree, "blockquote").border.widths(), Edges::all(2.25));
2695    }
2696
2697    /// `padding` reads the one-to-four value shorthand the way
2698    /// `margin` does, and its longhands overwrite what it set.
2699    #[test]
2700    fn padding_reads_the_shorthand_and_its_longhands() {
2701        let book = sample();
2702        let tree = compile(&book, "p { padding: 6pt 12pt; padding-bottom: 0 }");
2703        let padding = first(&tree, "p").padding;
2704        assert_eq!(padding.top, 6.0);
2705        assert_eq!(padding.right, 12.0);
2706        assert_eq!(padding.bottom, 0.0);
2707        assert_eq!(padding.left, 12.0);
2708    }
2709
2710    /// `text-justify` and `hanging-punctuation` read as CSS writes
2711    /// them: the first a keyword, the second a set of them in any
2712    /// order.
2713    #[test]
2714    fn justification_and_hanging_marks_read_as_css_writes_them() {
2715        let book = sample();
2716        let tree = compile(
2717            &book,
2718            "book { text-justify: inter-character;\
2719                    hanging-punctuation: last first allow-end }",
2720        );
2721        let paragraph = first(&tree, "p");
2722        assert_eq!(paragraph.text_justify, TextJustify::InterCharacter);
2723        assert_eq!(
2724            paragraph.hanging_punctuation,
2725            HangingPunctuation {
2726                first: true,
2727                end: HangEnd::Allow,
2728                last: true,
2729            },
2730        );
2731        let plain = compile(&book, "book { hanging-punctuation: none }");
2732        assert_eq!(
2733            first(&plain, "p").hanging_punctuation,
2734            HangingPunctuation::NONE,
2735        );
2736    }
2737
2738    /// Generic families resolve through the registry's bindings, and
2739    /// slope and weight pick the nearest face in a family.
2740    #[test]
2741    fn families_resolve_through_the_registry() {
2742        let book = sample();
2743        let tree = compile(&book, "p { font-family: \"Nowhere\", monospace }");
2744        let paragraph = first(&tree, "p");
2745        assert_eq!(
2746            paragraph.font_family,
2747            vec![
2748                Family::Named("Nowhere".into()),
2749                Family::Generic(GenericFamily::Monospace),
2750            ]
2751        );
2752        assert_eq!(
2753            paragraph.font_id,
2754            registry().generic(GenericFamily::Monospace).unwrap()
2755        );
2756    }
2757
2758    /// A book of one paragraph, a scene break and a quotation: the
2759    /// blocks whose defaults the built-in sheet has opinions about.
2760    fn furnished() -> Book {
2761        let mut book = Book {
2762            metadata: Metadata::default(),
2763            sections: vec![Section {
2764                attributes: Default::default(),
2765                blocks: vec![
2766                    Block::Paragraph {
2767                        id: NodeId::UNASSIGNED,
2768                        inlines: vec![text("Before the break.")],
2769                        attributes: Attributes::default(),
2770                        position: None,
2771                        span: None,
2772                    },
2773                    Block::ThematicBreak {
2774                        id: NodeId::UNASSIGNED,
2775                        attributes: Attributes::default(),
2776                        position: None,
2777                        span: None,
2778                    },
2779                    Block::Blockquote {
2780                        id: NodeId::UNASSIGNED,
2781                        blocks: vec![Block::Paragraph {
2782                            id: NodeId::UNASSIGNED,
2783                            inlines: vec![text("Quoted.")],
2784                            attributes: Attributes::default(),
2785                            position: None,
2786                            span: None,
2787                        }],
2788                        attributes: Attributes::default(),
2789                        position: None,
2790                        span: None,
2791                    },
2792                ],
2793                ..Default::default()
2794            }],
2795        };
2796        book.assign_node_ids();
2797        book
2798    }
2799
2800    /// The built-in sheet sets a scene break in an ornament, centred,
2801    /// and keeps it off both ends of a page. An author sets it in
2802    /// something else, or in space.
2803    #[test]
2804    fn a_thematic_break_takes_its_ornament_from_the_cascade() {
2805        let book = furnished();
2806        let tree = defaults(&book, registry());
2807        let rule = first(&tree, "hr");
2808        assert_eq!(rule.content, Content::Text("\u{2766}".into()));
2809        assert_eq!(rule.text_align, TextAlign::Center);
2810        assert_eq!(rule.break_before, Break::Avoid);
2811        assert_eq!(rule.break_after, Break::Avoid);
2812        assert_eq!(rule.margin.top, 11.0);
2813
2814        assert_eq!(
2815            first(&compile(&book, "hr { content: none }"), "hr").content,
2816            Content::None,
2817        );
2818        assert_eq!(
2819            first(&compile(&book, "hr { content: \"* * *\" }"), "hr").content,
2820            Content::Text("* * *".into()),
2821        );
2822    }
2823
2824    /// A quotation is indented on both sides and set off above and
2825    /// below, and the indent is a multiple of its own size.
2826    #[test]
2827    fn a_blockquote_is_indented_by_the_built_in_sheet() {
2828        let book = furnished();
2829        let tree = defaults(&book, registry());
2830        let quote = first(&tree, "blockquote");
2831        assert_eq!(quote.margin.left, 2.0 * quote.font_size);
2832        assert_eq!(quote.margin.right, 2.0 * quote.font_size);
2833        assert_eq!(quote.margin.top, quote.font_size);
2834        // The paragraph inside has no indent of its own: the
2835        // quotation's box is what moves it.
2836        assert_eq!(nth(&tree, "p", 1).margin.left, 0.0);
2837    }
2838
2839    /// `::first-letter` is a second pass over the element it belongs
2840    /// to: it starts from that element's own computed style, cascades
2841    /// the rules that named it over the top, and resolves a face of
2842    /// its own. Rules that name no pseudo-element never reach it, and
2843    /// it never reaches the element.
2844    #[test]
2845    fn first_letter_cascades_over_the_element_it_belongs_to() {
2846        let book = sample();
2847        let tree = compile(
2848            &book,
2849            "p { font-size: 12pt; line-height: 1.8 }
2850             p::first-letter { initial-letter: 3; font-family: monospace }",
2851        );
2852        let Block::Paragraph { id, .. } = &book.sections[0].blocks[1] else {
2853            panic!("the second block is a paragraph");
2854        };
2855        let initial = tree
2856            .first_letter(*id)
2857            .expect("a rule named the paragraph's first letter");
2858        assert_eq!(initial.initial_letter, 3);
2859        assert_eq!(initial.font_size, 12.0, "the element's size is inherited");
2860        assert_eq!(initial.line_height, 1.8);
2861        assert_eq!(
2862            initial.font_id,
2863            registry().generic(GenericFamily::Monospace).unwrap(),
2864            "the pseudo-element resolves its own face",
2865        );
2866
2867        // The element keeps its own style, and an element no rule
2868        // named has no first-letter style at all.
2869        assert_eq!(first(&tree, "p").initial_letter, 0);
2870        let Block::Heading { id, .. } = &book.sections[0].blocks[0] else {
2871            panic!("the first block is a heading");
2872        };
2873        assert!(tree.first_letter(*id).is_none());
2874        assert!(defaults(&book, registry()).first_letter(*id).is_none());
2875
2876        // Selectors in front of the pseudo-element still have to
2877        // match: only the paragraph after the heading is picked out.
2878        let tree = compile(&book, "h1 + p::first-letter { initial-letter: 2 }");
2879        assert!(tree.first_letter(*id).is_none());
2880        let Block::Paragraph { id: second, .. } = &book.sections[0].blocks[2] else {
2881            panic!("the third block is a paragraph");
2882        };
2883        assert!(tree.first_letter(*second).is_none());
2884    }
2885
2886    /// A pseudo-element outside the subset is a diagnostic, and the
2887    /// sheet keeps parsing.
2888    #[test]
2889    fn an_unsupported_pseudo_element_warns() {
2890        let book = sample();
2891        let tree = compile(
2892            &book,
2893            "p::marker { content: \"x\" }\np { font-size: 15pt }\n",
2894        );
2895        assert!(
2896            tree.warnings()
2897                .iter()
2898                .any(|warning| warning.message
2899                    == "Unsupported selector `:marker`. The rule is ignored."),
2900            "{:?}",
2901            tree.warnings(),
2902        );
2903        assert_eq!(first(&tree, "p").font_size, 15.0);
2904    }
2905
2906    /// A chapter heading named `the-hunter`, and a paragraph with a
2907    /// link to it.
2908    fn linked() -> Book {
2909        let mut book = sample();
2910        let blocks = &mut book.sections[0].blocks;
2911        if let Block::Heading { attributes, .. } = &mut blocks[0] {
2912            attributes.id = Some("the-hunter".into());
2913        }
2914        blocks.push(Block::Paragraph {
2915            id: NodeId::UNASSIGNED,
2916            inlines: vec![
2917                text("See "),
2918                Inline::Link {
2919                    id: NodeId::UNASSIGNED,
2920                    url: "#the-hunter".into(),
2921                    children: vec![text("the hunter")],
2922                    attributes: Attributes::default(),
2923                    position: None,
2924                    span: None,
2925                },
2926                text("."),
2927            ],
2928            attributes: Attributes::default(),
2929            position: None,
2930            span: None,
2931        });
2932        book.assign_node_ids();
2933        book
2934    }
2935
2936    /// The id of the first element of one name.
2937    fn id_of(tree: &StyleTree, element: &str) -> NodeId {
2938        let node = tree
2939            .nodes()
2940            .iter()
2941            .find(|node| node.element == element)
2942            .unwrap_or_else(|| panic!("no {element}"));
2943        NodeId::new(node.id)
2944    }
2945
2946    /// `content` on a pseudo-element reads literals and references in
2947    /// the order they are written. Literals side by side are one.
2948    #[test]
2949    fn content_reads_references_between_literals() {
2950        let book = linked();
2951        let after = |css: &str| {
2952            let tree = compile(&book, &format!("a::after {{ content: {css} }}"));
2953            assert!(tree.warnings().is_empty(), "{css}: {:?}", tree.warnings());
2954            tree.after(id_of(&tree, "a"))
2955                .map(|style| style.content.clone())
2956        };
2957        assert_eq!(
2958            after("\" (page \" target-counter(attr(href url), page) \")\""),
2959            Some(Content::Pieces(vec![
2960                ContentPiece::Text(" (page ".into()),
2961                ContentPiece::TargetCounter {
2962                    target: Target::Href,
2963                    style: CounterStyle::Decimal,
2964                },
2965                ContentPiece::Text(")".into()),
2966            ])),
2967        );
2968        assert_eq!(
2969            after("target-counter(\"#the-hunter\", page, upper-roman)"),
2970            Some(Content::Pieces(vec![ContentPiece::TargetCounter {
2971                target: Target::Url("#the-hunter".into()),
2972                style: CounterStyle::UpperRoman,
2973            }])),
2974        );
2975        assert_eq!(
2976            after("\"see \" target-text(attr(href))"),
2977            Some(Content::Pieces(vec![
2978                ContentPiece::Text("see ".into()),
2979                ContentPiece::TargetText {
2980                    target: Target::Href,
2981                },
2982            ])),
2983        );
2984        assert_eq!(after("\"a\" \"b\""), Some(Content::Text("ab".into())));
2985        assert_eq!(after("none"), None, "`none` generates nothing");
2986    }
2987
2988    /// `page` is the only counter a reference can print, and a
2989    /// function outside the subset is a diagnostic on `content`.
2990    #[test]
2991    fn a_reference_outside_the_subset_warns() {
2992        let book = linked();
2993        for css in [
2994            "a::after { content: target-counter(attr(href url), chapter) }",
2995            "a::after { content: target-counters(attr(href url), page, \".\") }",
2996            "a::after { content: target-counter(attr(title), page) }",
2997            "a::after { content: counter(page) }",
2998        ] {
2999            let tree = compile(&book, css);
3000            assert!(
3001                tree.warnings().iter().any(|warning| warning.message
3002                    == "Unsupported value for `content`. The declaration is ignored."),
3003                "{css}: {:?}",
3004                tree.warnings(),
3005            );
3006            assert!(tree.after(id_of(&tree, "a")).is_none(), "{css}");
3007        }
3008    }
3009
3010    /// A pseudo-element starts from the style of the element it
3011    /// belongs to, and a rule that names it outranks what it
3012    /// inherited.
3013    #[test]
3014    fn a_pseudo_element_inherits_from_its_element() {
3015        let book = linked();
3016        let tree = compile(
3017            &book,
3018            "a { font-size: 9pt; font-family: monospace; color: #336699 }
3019             a::before { content: \"[\" }
3020             a::after { content: \"]\"; color: #d6075e; font-size: 7pt }",
3021        );
3022        let link = id_of(&tree, "a");
3023        let before = tree.before(link).expect("`::before` generates text");
3024        assert_eq!(before.content, Content::Text("[".into()));
3025        assert_eq!(before.font_size, 9.0);
3026        assert_eq!(before.color, Color::rgb(0x33, 0x66, 0x99));
3027        assert_eq!(
3028            before.font_id,
3029            registry().generic(GenericFamily::Monospace).unwrap(),
3030        );
3031        let after = tree.after(link).expect("`::after` generates text");
3032        assert_eq!(after.font_size, 7.0);
3033        assert_eq!(after.color, Color::rgb(0xd6, 0x07, 0x5e));
3034
3035        // The element keeps its own style, and generates nothing
3036        // itself.
3037        assert_eq!(tree.style(link).content, Content::None);
3038        assert_eq!(tree.style(link).font_size, 9.0);
3039    }
3040
3041    /// `content: none` is the initial value, so a rule that names a
3042    /// pseudo-element and sets no `content` generates nothing.
3043    #[test]
3044    fn a_pseudo_element_with_no_content_generates_nothing() {
3045        let book = linked();
3046        let tree = compile(
3047            &book,
3048            "a::after { color: red } em::before { content: none } \
3049             p::after { height: 2pt } h1::before { content: none; height: 2pt }",
3050        );
3051        assert!(tree.after(id_of(&tree, "a")).is_none());
3052        assert!(tree.before(id_of(&tree, "em")).is_none());
3053        assert!(tree.after(id_of(&tree, "p")).is_none());
3054        assert!(tree.before(id_of(&tree, "h1")).is_none());
3055        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
3056    }
3057
3058    /// Part: `::before` and `::after` parse and match on a block, and
3059    /// the cascade computes a style for each.
3060    #[test]
3061    fn a_pseudo_element_on_a_block_matches_and_computes_a_style() {
3062        let book = linked();
3063        let tree = compile(
3064            &book,
3065            "p::before { content: \"x\"; color: red }\np { font-size: 15pt }\n\
3066             h1::after { content: \"\"; height: 2pt }",
3067        );
3068        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
3069        let before = tree
3070            .before(id_of(&tree, "p"))
3071            .expect("`p::before` generates");
3072        assert_eq!(before.content, Content::Text("x".into()));
3073        assert_eq!(before.color, Color::rgb(0xff, 0, 0));
3074        let after = tree
3075            .after(id_of(&tree, "h1"))
3076            .expect("`h1::after` generates");
3077        assert_eq!(after.content, Content::Text(String::new()));
3078        assert_eq!(after.height, Width::Points(2.0));
3079        assert!(tree.after(id_of(&tree, "p")).is_none());
3080        assert_eq!(first(&tree, "p").content, Content::None);
3081    }
3082
3083    /// Part: the style tree gives `::first-letter`, `::first-line`,
3084    /// `::before` and `::after` an id that no content node can have.
3085    /// The same book and stylesheet give the same ids.
3086    #[test]
3087    fn a_pseudo_element_has_an_id_no_content_node_has() {
3088        let book = linked();
3089        let css = "p::before { content: \"x\" } h1::after { content: \"\"; height: 2pt } \
3090                   p::first-letter { initial-letter: 3 } \
3091                   p::first-line { font-variant-caps: small-caps }";
3092        let tree = compile(&book, css);
3093        let p = id_of(&tree, "p");
3094        let before = tree
3095            .pseudo_element(p, PseudoElement::Before)
3096            .expect("`p::before` generates");
3097        assert_eq!(tree.pseudo_element(p, PseudoElement::After), None);
3098        assert_eq!(before.pseudo_element(), Some((p, PseudoElement::Before)));
3099        assert_eq!(before.element(), p);
3100        assert_eq!(p.pseudo_element(), None);
3101        assert_eq!(p.element(), p);
3102        assert_eq!(book.subtree(before), None, "the book holds no such node");
3103        assert!(tree.nodes().iter().all(|node| node.id != before.get()));
3104        assert_eq!(tree.style(before).content, Content::Text("x".into()));
3105
3106        let h1 = id_of(&tree, "h1");
3107        let after = tree
3108            .pseudo_element(h1, PseudoElement::After)
3109            .expect("`h1::after` generates");
3110        assert_ne!(after, before);
3111        assert_eq!(tree.style(after).height, Width::Points(2.0));
3112
3113        let letter = tree
3114            .pseudo_element(p, PseudoElement::FirstLetter)
3115            .expect("`p::first-letter` is styled");
3116        let line = tree
3117            .pseudo_element(p, PseudoElement::FirstLine)
3118            .expect("`p::first-line` is styled");
3119        assert_eq!(tree.pseudo_element(h1, PseudoElement::FirstLetter), None);
3120        let ids = [before, after, letter, line];
3121        for id in ids {
3122            assert_eq!(ids.iter().filter(|other| **other == id).count(), 1);
3123            assert_eq!(book.subtree(id), None, "the book holds no such node");
3124            assert!(tree.nodes().iter().all(|node| node.id != id.get()));
3125        }
3126        assert_eq!(letter.element(), p);
3127        assert_eq!(line.element(), p);
3128        assert!(std::ptr::eq(
3129            tree.style(letter),
3130            tree.first_letter(p).unwrap()
3131        ));
3132        assert!(std::ptr::eq(tree.style(line), tree.first_line(p).unwrap()));
3133
3134        let again = compile(&book, css);
3135        assert_eq!(again.pseudo_element(p, PseudoElement::Before), Some(before));
3136        assert_eq!(again.pseudo_element(h1, PseudoElement::After), Some(after));
3137        assert_eq!(
3138            again.pseudo_element(p, PseudoElement::FirstLetter),
3139            Some(letter)
3140        );
3141        assert_eq!(
3142            again.pseudo_element(p, PseudoElement::FirstLine),
3143            Some(line)
3144        );
3145    }
3146
3147    /// Part: `::before` and `::after` on an inline element have an id,
3148    /// the same as on a block.
3149    #[test]
3150    fn an_inline_pseudo_element_has_an_id_as_a_block_does() {
3151        let book = linked();
3152        let tree = compile(&book, "a::after { content: \" (link)\" }");
3153        let a = id_of(&tree, "a");
3154        let after = tree
3155            .pseudo_element(a, PseudoElement::After)
3156            .expect("`a::after` generates text");
3157        assert_eq!(after.pseudo_element(), Some((a, PseudoElement::After)));
3158        assert_eq!(after.element(), a);
3159        assert_eq!(tree.pseudo_element(a, PseudoElement::Before), None);
3160        assert_eq!(tree.style(after).content, Content::Text(" (link)".into()));
3161    }
3162
3163    /// An element with no box of its own takes no generated box. A
3164    /// sheet that asks for one is told so once, and the element is
3165    /// styled as though the rule were not there.
3166    #[test]
3167    fn a_pseudo_element_on_the_book_warns_and_generates_nothing() {
3168        let book = linked();
3169        let tree = compile(
3170            &book,
3171            "book::before { content: \"x\" }\nbook { font-size: 15pt }",
3172        );
3173        let told: Vec<&str> = tree
3174            .warnings()
3175            .iter()
3176            .map(|warning| warning.message.as_str())
3177            .collect();
3178        assert_eq!(
3179            told,
3180            ["Unsupported pseudo-element `::before` on `book`. Nothing is generated."],
3181        );
3182        assert!(tree.before(id_of(&tree, "book")).is_none());
3183        assert_eq!(first(&tree, "book").font_size, 15.0);
3184    }
3185
3186    /// Acceptance: a pseudo-element inherits the element's family and
3187    /// size, and a rule naming the pseudo-element beats it, however
3188    /// specific the rule on the element is.
3189    #[test]
3190    fn a_block_pseudo_element_inherits_and_its_own_rule_beats_the_element() {
3191        let book = linked();
3192        let tree = compile(
3193            &book,
3194            "section > h1#the-hunter { font-size: 20pt; font-family: monospace }\n\
3195             h1::before { content: \"x\" }\nh1::after { content: \"y\"; font-size: 8pt }",
3196        );
3197        let h1 = id_of(&tree, "h1");
3198        let mono = registry().generic(GenericFamily::Monospace).unwrap();
3199        let before = tree.before(h1).expect("`::before` generates");
3200        assert_eq!(before.font_size, 20.0);
3201        assert_eq!(before.font_id, mono);
3202        let after = tree.after(h1).expect("`::after` generates");
3203        assert_eq!(after.font_size, 8.0);
3204        assert_eq!(after.font_id, mono);
3205    }
3206
3207    /// Only a reference to a page asks for a second layout pass. The
3208    /// text of the target is in the book before anything is laid out.
3209    #[test]
3210    fn only_a_page_reference_counts_pages() {
3211        let book = linked();
3212        for (css, counts) in [
3213            ("", false),
3214            ("a::after { content: \" (see below)\" }", false),
3215            (
3216                "a::after { content: \" \" target-text(attr(href url)) }",
3217                false,
3218            ),
3219            (
3220                "a::after { content: \" \" target-counter(attr(href url), page) }",
3221                true,
3222            ),
3223            (
3224                "p::after { content: target-counter(\"#the-hunter\", page) }",
3225                true,
3226            ),
3227            ("p::after { content: target-text(\"#the-hunter\") }", false),
3228        ] {
3229            assert_eq!(compile(&book, css).counts_pages(), counts, "{css}");
3230        }
3231    }
3232
3233    /// `::first-line` cascades over the element the way
3234    /// `::first-letter` does, and what line layout reads back is what
3235    /// it changed rather than the whole computed style.
3236    #[test]
3237    fn first_line_cascades_over_the_element_it_belongs_to() {
3238        let book = sample();
3239        let tree = compile(
3240            &book,
3241            "p { font-size: 12pt; color: #112233 }
3242             h1 + p::first-line { font-variant-caps: small-caps; letter-spacing: 0.5pt }",
3243        );
3244        let Block::Paragraph { id, .. } = &book.sections[0].blocks[1] else {
3245            panic!("the second block is a paragraph");
3246        };
3247        let line = tree
3248            .first_line(*id)
3249            .expect("a rule named the paragraph's first line");
3250        assert_eq!(line.font_variant_caps, FontVariantCaps::SmallCaps);
3251        assert_eq!(line.font_size, 12.0, "the element's size is inherited");
3252        assert_eq!(line.color, first(&tree, "p").color);
3253
3254        let opening = tree.opening_line(*id).expect("the opening line's overlay");
3255        assert_eq!(opening.caps, Some(FontVariantCaps::SmallCaps));
3256        assert_eq!(opening.letter_spacing, Some(0.5));
3257        assert_eq!(opening.size, None, "nothing the rule left alone is set");
3258        assert_eq!(opening.color, None);
3259        assert_eq!(opening.transform, None);
3260
3261        // The element keeps its own style, and the sibling selector
3262        // in front of the pseudo-element still has to match.
3263        assert_eq!(first(&tree, "p").font_variant_caps, FontVariantCaps::Normal);
3264        let Block::Paragraph { id: second, .. } = &book.sections[0].blocks[2] else {
3265            panic!("the third block is a paragraph");
3266        };
3267        assert!(tree.first_line(*second).is_none());
3268        assert!(tree.opening_line(*second).is_none());
3269    }
3270
3271    /// `::first-line` takes the properties that change a run's width
3272    /// and its colour. Anything else warns naming the property, is
3273    /// dropped, and leaves the rest of the rule standing.
3274    #[test]
3275    fn a_property_outside_the_first_line_set_warns_and_is_dropped() {
3276        let book = sample();
3277        let tree = compile(
3278            &book,
3279            "p::first-line { line-height: 2; text-transform: uppercase }",
3280        );
3281        assert!(
3282            tree.warnings().iter().any(|warning| {
3283                warning.message
3284                    == "Unsupported property `line-height` on `::first-line`. \
3285                        The declaration is ignored."
3286            }),
3287            "{:?}",
3288            tree.warnings(),
3289        );
3290        let Block::Paragraph { id, .. } = &book.sections[0].blocks[1] else {
3291            panic!("the second block is a paragraph");
3292        };
3293        let opening = tree.opening_line(*id).expect("the opening line's overlay");
3294        assert_eq!(opening.transform, Some(TextTransform::Uppercase));
3295        assert_eq!(
3296            tree.first_line(*id).unwrap().line_height,
3297            first(&tree, "p").line_height,
3298            "the line height the rule asked for was dropped",
3299        );
3300    }
3301
3302    /// Acceptance: `font-family`, `font-style` and `font-weight` on
3303    /// `::first-line` warn about none of themselves, and each one
3304    /// reaches the overlay line layout reads.
3305    #[test]
3306    fn the_font_properties_set_on_a_first_line() {
3307        let book = sample();
3308        let mut registry = bundled_registry().expect("bundled font parses");
3309        let mut display =
3310            crate::fonts::FontSource::from_bytes(crate::fonts::BUNDLED_ITALIC.to_vec())
3311                .expect("the face parses");
3312        display.family = "display".into();
3313        registry.add(display).expect("the face registers");
3314        let css = "p { font-family: \"EB Garamond\" }
3315                   p::first-line { font-family: display; font-style: italic;
3316                                   font-weight: bold }";
3317        let tree =
3318            Stylesheets::parse(&[Source::author("author.css", css)]).compile(&book, &registry);
3319        assert_eq!(
3320            tree.warnings()
3321                .iter()
3322                .filter(|warning| warning.message.contains("`::first-line`"))
3323                .collect::<Vec<_>>(),
3324            Vec::<&Warning>::new(),
3325        );
3326
3327        let Block::Paragraph { id, .. } = &book.sections[0].blocks[1] else {
3328            panic!("the second block is a paragraph");
3329        };
3330        let face = tree
3331            .opening_line(*id)
3332            .expect("the opening line's overlay")
3333            .face
3334            .expect("the rule asked for another face");
3335        assert_eq!(face.italic, Some(true));
3336        assert_eq!(face.weight, Some(700));
3337        assert_eq!(
3338            registry
3339                .font_ref(face.family.expect("the rule named a family"))
3340                .map(|entry| entry.family.as_str()),
3341            Some("display"),
3342        );
3343    }
3344
3345    /// A book whose one paragraph nests `strong` inside `em`.
3346    fn nested() -> Book {
3347        let mut book = Book {
3348            metadata: Metadata::default(),
3349            sections: vec![Section {
3350                attributes: Default::default(),
3351                blocks: vec![Block::Paragraph {
3352                    id: NodeId::UNASSIGNED,
3353                    inlines: vec![
3354                        text("She said "),
3355                        Inline::Emphasis {
3356                            id: NodeId::UNASSIGNED,
3357                            children: vec![
3358                                text("never "),
3359                                Inline::Strong {
3360                                    id: NodeId::UNASSIGNED,
3361                                    children: vec![text("again")],
3362                                    attributes: Attributes::default(),
3363                                    position: None,
3364                                    span: None,
3365                                },
3366                            ],
3367                            attributes: Attributes::default(),
3368                            position: None,
3369                            span: None,
3370                        },
3371                        text("."),
3372                    ],
3373                    attributes: Attributes::default(),
3374                    position: None,
3375                    span: None,
3376                }],
3377                ..Default::default()
3378            }],
3379        };
3380        book.assign_node_ids();
3381        book
3382    }
3383
3384    /// Slope and weight reach the face through the cascade: `em` and
3385    /// `strong` set properties like any other rule, and `strong`
3386    /// inside `em` inherits the slope on its way to a bold italic
3387    /// face.
3388    #[test]
3389    fn emphasis_and_strong_resolve_to_their_own_faces() {
3390        let book = nested();
3391        let tree = defaults(&book, registry());
3392        let face = |italic, weight| {
3393            registry()
3394                .select("eb garamond", FaceAttributes { italic, weight })
3395                .unwrap()
3396                .id
3397        };
3398        assert_eq!(first(&tree, "p").font_id, face(false, 400));
3399        assert_eq!(first(&tree, "em").font_id, face(true, 400));
3400        assert_eq!(first(&tree, "strong").font_id, face(true, 700));
3401        // The cascade, not the element name: an author who unsets
3402        // the slope gets the upright bold cut under the same markup.
3403        let tree = compile(&book, "em { font-style: normal }");
3404        assert_eq!(first(&tree, "strong").font_id, face(false, 700));
3405    }
3406
3407    /// A face the registry cannot supply is a diagnostic and the
3408    /// nearest cut, once, however many nodes asked for it.
3409    #[test]
3410    fn a_face_the_registry_lacks_warns_and_lays_out_anyway() {
3411        let mut upright = FontRegistry::new();
3412        upright
3413            .add(crate::fonts::FontSource::from_bytes(BUNDLED_FONT.to_vec()).unwrap())
3414            .unwrap();
3415        upright
3416            .map_generic(GenericFamily::Serif, "eb garamond")
3417            .unwrap();
3418        let book = nested();
3419        let tree = Stylesheets::parse(&[]).compile(&book, &upright);
3420        assert_eq!(
3421            first(&tree, "em").font_id,
3422            first(&tree, "p").font_id,
3423            "there is no italic cut, so emphasis takes the upright one",
3424        );
3425        let complaints: Vec<&str> = tree
3426            .warnings()
3427            .iter()
3428            .map(|warning| warning.message.as_str())
3429            .filter(|message| message.contains("italic"))
3430            .collect();
3431        assert_eq!(
3432            complaints,
3433            vec!["eb garamond has no italic face. Italic text falls back to upright."],
3434        );
3435
3436        // A stack that resolves nothing at all says so, and the book
3437        // still lays out on the first registered face.
3438        let tree = compile(&book, "p { font-family: \"Nowhere\", \"Nor here\" }");
3439        assert_eq!(first(&tree, "p").font_id, 0);
3440        assert!(
3441            tree.warnings().iter().any(|warning| {
3442                warning.message
3443                    == "No registered font matches Nowhere, Nor here. \
3444                        The first registered font is used instead."
3445            }),
3446            "{:?}",
3447            tree.warnings(),
3448        );
3449    }
3450
3451    /// The display-typography properties parse, cascade and inherit:
3452    /// a rule on the book reaches the paragraph inside it, a rule on
3453    /// the paragraph overrides it, and `em` inside picks up what the
3454    /// paragraph set without being named.
3455    #[test]
3456    fn display_typography_parses_cascades_and_inherits() {
3457        let book = nested();
3458        let tree = compile(
3459            &book,
3460            "book { letter-spacing: 0.1em; font-variant-caps: small-caps;
3461                    text-transform: uppercase }
3462             p { letter-spacing: 2pt }",
3463        );
3464        let paragraph = first(&tree, "p");
3465        assert_eq!(paragraph.letter_spacing, 2.0, "the closer rule lost");
3466        assert_eq!(paragraph.font_variant_caps, FontVariantCaps::SmallCaps);
3467        assert_eq!(paragraph.text_transform, TextTransform::Uppercase);
3468        let emphasis = first(&tree, "em");
3469        assert_eq!(
3470            (
3471                emphasis.letter_spacing,
3472                emphasis.font_variant_caps,
3473                emphasis.text_transform,
3474            ),
3475            (2.0, FontVariantCaps::SmallCaps, TextTransform::Uppercase,),
3476            "emphasis did not inherit what the paragraph set",
3477        );
3478
3479        // `em` is a multiple of the element's own size, `normal` and
3480        // `none` are the initial values written out, and the defaults
3481        // are what a book gets when nothing says otherwise.
3482        let tree = compile(
3483            &book,
3484            "book { letter-spacing: 0.1em; text-transform: uppercase }
3485             p { font-size: 20pt; letter-spacing: 0.1em }
3486             em { letter-spacing: normal; text-transform: none;
3487                  font-variant-caps: normal }",
3488        );
3489        assert_eq!(first(&tree, "p").letter_spacing, 2.0);
3490        assert_eq!(first(&tree, "em").letter_spacing, 0.0);
3491        assert_eq!(first(&tree, "em").text_transform, TextTransform::None);
3492        let plain = first(&defaults(&book, registry()), "p");
3493        assert_eq!(plain.letter_spacing, 0.0);
3494        assert_eq!(plain.font_variant_caps, FontVariantCaps::Normal);
3495        assert_eq!(plain.text_transform, TextTransform::None);
3496
3497        // A value none of the three has is a diagnostic, not a rule.
3498        let tree = compile(&book, "p { text-transform: full-width }");
3499        assert!(
3500            tree.warnings().iter().any(|warning| {
3501                warning.message
3502                    == "Unsupported value for `text-transform`. The declaration is ignored."
3503            }),
3504            "{:?}",
3505            tree.warnings(),
3506        );
3507    }
3508
3509    /// Part: `font-feature-settings` reads a tag and a value, in
3510    /// each of the three ways a value is written, and it inherits.
3511    #[test]
3512    fn font_feature_settings_read_a_tag_and_a_value_and_inherit() {
3513        let book = nested();
3514        let tree = compile(
3515            &book,
3516            "book { font-feature-settings: \"ss01\" 1, \"liga\" off, \"onum\" }",
3517        );
3518        let asked = [
3519            FeatureSetting::new(*b"ss01", 1),
3520            FeatureSetting::new(*b"liga", 0),
3521            FeatureSetting::new(*b"onum", 1),
3522        ];
3523        assert_eq!(first(&tree, "p").font_feature_settings, asked);
3524        assert_eq!(
3525            first(&tree, "em").font_feature_settings,
3526            asked,
3527            "emphasis did not inherit what the book asked for",
3528        );
3529        assert!(
3530            first(&defaults(&book, registry()), "p")
3531                .font_feature_settings
3532                .is_empty(),
3533            "a book that asks for nothing asks for nothing",
3534        );
3535
3536        let tree = compile(&book, "p { font-feature-settings: normal }");
3537        assert!(first(&tree, "p").font_feature_settings.is_empty());
3538
3539        let tree = compile(&book, "p { font-feature-settings: ss01 }");
3540        assert!(
3541            tree.warnings().iter().any(|warning| warning.message
3542                == "Unsupported value for `font-feature-settings`. The declaration is ignored."),
3543            "{:?}",
3544            tree.warnings(),
3545        );
3546    }
3547
3548    /// Part: the `font-variant` longhands name a feature without
3549    /// spelling its tag, and the computed list merges over what the
3550    /// engine asks for, `font-feature-settings` last.
3551    #[test]
3552    fn the_font_variant_longhands_name_the_features_they_stand_for() {
3553        let book = nested();
3554        let tree = compile(
3555            &book,
3556            "p { font-variant-numeric: oldstyle-nums tabular-nums;
3557                 font-variant-ligatures: no-common-ligatures discretionary-ligatures;
3558                 font-variant-alternates: historical-forms;
3559                 font-feature-settings: \"onum\" 0, \"ss01\" 1 }",
3560        );
3561        let paragraph = first(&tree, "p");
3562        assert_eq!(
3563            paragraph.features(),
3564            [
3565                FeatureSetting::new(*b"liga", 0),
3566                FeatureSetting::new(*b"clig", 0),
3567                FeatureSetting::new(*b"dlig", 1),
3568                FeatureSetting::new(*b"onum", 1),
3569                FeatureSetting::new(*b"tnum", 1),
3570                FeatureSetting::new(*b"hist", 1),
3571                FeatureSetting::new(*b"onum", 0),
3572                FeatureSetting::new(*b"ss01", 1),
3573            ],
3574            "the sheet's own settings are read after the named forms",
3575        );
3576
3577        let plain = first(&defaults(&book, registry()), "p");
3578        assert!(plain.features().is_empty());
3579    }
3580
3581    /// Part: a face that carries none of a feature the sheet asked
3582    /// for is named once, and a feature turned off is not.
3583    #[test]
3584    fn a_face_without_a_requested_feature_is_named() {
3585        let book = nested();
3586        let tree = compile(
3587            &book,
3588            "book { font-feature-settings: \"zero\" 1 }
3589             p { font-feature-settings: \"zero\" 1, \"ss01\" 1 }",
3590        );
3591        let named: Vec<&str> = tree
3592            .warnings()
3593            .iter()
3594            .map(|warning| warning.message.as_str())
3595            .filter(|message| message.contains("feature"))
3596            .collect();
3597        assert_eq!(
3598            named,
3599            ["eb garamond has no `zero` feature. The text is set without it."],
3600            "{:?}",
3601            tree.warnings(),
3602        );
3603
3604        let quiet = compile(&book, "p { font-feature-settings: \"zero\" 0 }");
3605        assert!(
3606            !quiet
3607                .warnings()
3608                .iter()
3609                .any(|warning| warning.message.contains("feature")),
3610            "a feature asked to stay off is one the face need not carry: {:?}",
3611            quiet.warnings(),
3612        );
3613    }
3614
3615    /// `color` reads in each of the four forms the subset takes, and
3616    /// a form outside it is a diagnostic rather than a rule.
3617    #[test]
3618    fn colour_reads_in_every_form_the_subset_takes() {
3619        let book = sample();
3620        let tree = compile(
3621            &book,
3622            "h1 { color: darkred }
3623             p { color: #036 }
3624             blockquote { color: #b41e1e }
3625             em { color: rgb(0% 20% 100%) }",
3626        );
3627        assert_eq!(first(&tree, "h1").color, Color::rgb(139, 0, 0));
3628        assert_eq!(first(&tree, "p").color, Color::rgb(0, 51, 102));
3629        assert_eq!(first(&tree, "blockquote").color, Color::rgb(180, 30, 30));
3630        assert_eq!(first(&tree, "em").color, Color::rgb(0, 51, 255));
3631        assert_eq!(
3632            first(&defaults(&book, registry()), "p").color,
3633            Color::BLACK,
3634            "a book nothing coloured is set in black",
3635        );
3636
3637        // A name no colour has, and channels written half as
3638        // numbers and half as percentages, neither of which CSS
3639        // allows.
3640        for css in ["p { color: octarine }", "p { color: rgb(10, 20%, 30) }"] {
3641            let tree = compile(&book, css);
3642            assert!(
3643                tree.warnings().iter().any(|warning| warning.message
3644                    == "Unsupported value for `color`. The declaration is ignored."),
3645                "{css}: {:?}",
3646                tree.warnings(),
3647            );
3648        }
3649    }
3650
3651    /// Colour inherits: a rule on the section reaches the prose under
3652    /// it, and a rule on the heading beats what it inherited.
3653    #[test]
3654    fn a_colour_on_a_section_reaches_the_prose_under_it() {
3655        let book = sample();
3656        let tree = compile(
3657            &book,
3658            "section { color: #444444 }
3659             h1 { color: rgb(180 30 30) }",
3660        );
3661        assert_eq!(first(&tree, "p").color, Color::rgb(68, 68, 68));
3662        assert_eq!(first(&tree, "em").color, Color::rgb(68, 68, 68));
3663        assert_eq!(first(&tree, "h1").color, Color::rgb(180, 30, 30));
3664    }
3665
3666    /// `color` is the ink a box is set in and `background-color` is
3667    /// what is painted behind it. The text colour inherits and the
3668    /// background does not, so the emphasis inside a tinted paragraph
3669    /// takes the ink and leaves the tint.
3670    #[test]
3671    fn colour_inherits_where_a_background_does_not() {
3672        let book = nested();
3673        let tree = compile(
3674            &book,
3675            "p {\n  color: teal;\n  background-color: #f4f1ea;\n}\n",
3676        );
3677        assert!(tree.warnings().is_empty(), "{:?}", tree.warnings());
3678        assert_eq!(first(&tree, "p").color, Color::rgb(0, 128, 128));
3679        assert_eq!(
3680            first(&tree, "p").background.color,
3681            Some(Color::rgb(0xf4, 0xf1, 0xea))
3682        );
3683        assert_eq!(first(&tree, "em").color, Color::rgb(0, 128, 128));
3684        assert_eq!(first(&tree, "em").background.color, None);
3685    }
3686
3687    /// A face with no small capitals of its own is reported once,
3688    /// however many nodes asked for them, and the book is set anyway.
3689    /// A face that has them is not reported at all.
3690    #[test]
3691    fn a_face_without_small_capitals_says_so() {
3692        let book = nested();
3693        let bare = crate::fonts::registry_without_substitutions();
3694        let css = "p, em, strong { font-variant-caps: small-caps }";
3695        let tree = Stylesheets::parse(&[Source::author("author.css", css)]).compile(&book, &bare);
3696        let complaints: Vec<&str> = tree
3697            .warnings()
3698            .iter()
3699            .map(|warning| warning.message.as_str())
3700            .filter(|message| message.contains("small capitals"))
3701            .collect();
3702        assert_eq!(
3703            complaints,
3704            vec!["eb garamond has no small capitals. Falling back to reduced capitals."],
3705        );
3706
3707        let tree = compile(&book, css);
3708        assert!(
3709            !tree
3710                .warnings()
3711                .iter()
3712                .any(|warning| warning.message.contains("small capitals")),
3713            "the bundled face has small capitals: {:?}",
3714            tree.warnings(),
3715        );
3716    }
3717
3718    /// Styles are interned: a book of thousands of nodes computes a
3719    /// handful of distinct styles, and every node points at one.
3720    #[test]
3721    fn styles_are_shared_between_nodes() {
3722        let book = sample();
3723        let tree = defaults(&book, registry());
3724        assert!(
3725            tree.styles().len() < tree.nodes().len(),
3726            "{} styles for {} nodes",
3727            tree.styles().len(),
3728            tree.nodes().len()
3729        );
3730        for node in tree.nodes() {
3731            assert!((node.style as usize) < tree.styles().len());
3732        }
3733        // A real node id resolves to the style its element got: the
3734        // section is the second element, after the book itself.
3735        assert_eq!(
3736            tree.style(book.sections[0].id),
3737            &tree.styles()[tree.nodes()[1].style as usize]
3738        );
3739    }
3740}