1mod 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
59pub const USER_AGENT_CSS: &str = include_str!("style/ua.css");
62
63pub trait FontLoader {
70 fn load(&self, url: &str) -> Option<Vec<u8>>;
73}
74
75pub struct NoFonts;
77
78impl FontLoader for NoFonts {
79 fn load(&self, _url: &str) -> Option<Vec<u8>> {
80 None
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize)]
86pub struct NodeStyle {
87 pub id: u32,
89 pub element: &'static str,
91 pub style: u32,
93 #[serde(skip_serializing_if = "Option::is_none")]
96 pub first_letter: Option<u32>,
97 #[serde(skip_serializing_if = "Option::is_none")]
100 pub first_line: Option<u32>,
101 #[serde(skip_serializing_if = "Option::is_none")]
104 pub before: Option<u32>,
105 #[serde(skip_serializing_if = "Option::is_none")]
108 pub after: Option<u32>,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
114#[serde(rename_all = "snake_case")]
115pub enum Situation {
116 First(Side),
118 Body(Side),
120 Blank,
122}
123
124impl Situation {
125 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct PageQuery<'a> {
138 pub name: Option<&'a str>,
140 pub situation: Situation,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize)]
147pub struct PageStyle {
148 pub geometry: PageGeometry,
150 #[serde(skip_serializing_if = "properties::no_background")]
152 pub background: Background,
153 pub boxes: Vec<MarginBoxStyle>,
155}
156
157impl PageStyle {
158 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#[derive(Debug, Clone, PartialEq, Serialize)]
168pub struct MarginBoxStyle {
169 pub which: MarginBox,
171 pub content: Content,
173 pub style: ComputedStyle,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize)]
180pub struct PageMaster {
181 pub page: Option<String>,
183 pub situation: Situation,
185 pub style: PageStyle,
187}
188
189#[derive(Debug, Clone, Serialize)]
191pub struct StyleTree {
192 styles: Vec<ComputedStyle>,
195 nodes: Vec<NodeStyle>,
197 masters: Vec<PageMaster>,
199 #[serde(skip)]
202 by_node: Vec<u32>,
203 #[serde(skip)]
205 initial_by_node: Vec<Option<u32>>,
206 #[serde(skip)]
208 first_line_by_node: Vec<Option<u32>>,
209 #[serde(skip)]
211 before_by_node: Vec<Option<u32>>,
212 #[serde(skip)]
214 after_by_node: Vec<Option<u32>>,
215 #[serde(skip)]
217 counts_pages: bool,
218 #[serde(skip)]
220 refers: bool,
221 #[serde(skip)]
223 notes: NodeId,
224 warnings: Vec<Warning>,
225}
226
227impl StyleTree {
228 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 pub fn notes_area(&self) -> NodeId {
249 self.notes
250 }
251
252 pub fn paragraph(&self, id: NodeId) -> ParagraphStyle {
254 self.style(id).paragraph()
255 }
256
257 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 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 pub fn opening_line(&self, id: NodeId) -> Option<FirstLine> {
275 Some(self.first_line(id)?.first_line_over(self.style(id)))
276 }
277
278 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 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 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 pub fn counts_pages(&self) -> bool {
316 self.counts_pages
317 }
318
319 pub fn refers(&self) -> bool {
322 self.refers
323 }
324
325 pub fn root(&self) -> &ComputedStyle {
327 &self.styles[0]
328 }
329
330 pub fn styles(&self) -> &[ComputedStyle] {
332 &self.styles
333 }
334
335 pub fn nodes(&self) -> &[NodeStyle] {
337 &self.nodes
338 }
339
340 pub fn warnings(&self) -> &[Warning] {
342 &self.warnings
343 }
344
345 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 pub fn default_page(&self) -> &PageStyle {
356 self.page(PageQuery {
357 name: None,
358 situation: Situation::Body(Side::Recto),
359 })
360 }
361
362 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
376fn 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
422fn 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 matching.sort_by_key(|(level, rule)| (*level, rule.specificity()));
439
440 let root_size = root.font_size;
441 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
514fn 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
560fn 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
594fn 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
617fn 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#[derive(Debug)]
641pub struct Stylesheets {
642 sheets: Vec<Sheet>,
643 warnings: Vec<Warning>,
645 parsed: usize,
647}
648
649impl Stylesheets {
650 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 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 pub(crate) fn font_warnings(&self) -> &[Warning] {
687 &self.warnings[self.parsed..]
688 }
689
690 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 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 pub fn warnings(&self) -> &[Warning] {
721 &self.warnings
722 }
723
724 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 pub fn compile(&self, book: &Book, registry: &FontRegistry) -> StyleTree {
764 cascade(book, &self.sheets, registry, self.warnings.clone())
765 }
766}
767
768#[derive(Debug, Clone, PartialEq)]
770pub struct WrittenSheet {
771 pub origin: Origin,
773 pub rules: Vec<WrittenRule>,
775 pub pages: Vec<WrittenPage>,
777 pub faces: Vec<FontFace>,
779}
780
781#[derive(Debug, Clone, PartialEq)]
783pub struct WrittenRule {
784 pub selectors: Vec<String>,
786 pub declarations: Vec<Written>,
789 pub position: SheetPosition,
791}
792
793#[derive(Debug, Clone, PartialEq)]
795pub struct WrittenPage {
796 pub selector: String,
798 pub position: SheetPosition,
800}
801
802pub fn defaults(book: &Book, registry: &FontRegistry) -> StyleTree {
804 Stylesheets::parse(&[]).compile(book, registry)
805}
806
807fn 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
849const FALLBACK: &str = "Falling back to another registered font.";
852
853fn 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
864fn 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
875fn 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 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 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 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 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
1080fn 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(§ion.blocks, source, &mut first, &mut warnings);
1089 }
1090 warnings
1091}
1092
1093fn 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
1152fn 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
1178fn 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
1199type Applicable = (u8, u32, usize, usize, usize);
1202
1203fn 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
1256fn 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 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
1309fn 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
1318fn 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
1330fn 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
1342fn 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
1354fn report_all(warnings: &mut Vec<Warning>, found: Vec<Warning>) {
1356 for warning in found {
1357 report(warnings, Some(warning));
1358 }
1359}
1360
1361fn 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
1407fn 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
1424fn 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
1453fn 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
1467fn 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 #[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 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 fn compile(book: &Book, css: &str) -> StyleTree {
1616 Stylesheets::parse(&[Source::author("author.css", css)]).compile(book, registry())
1617 }
1618
1619 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 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 #[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 #[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 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 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 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 #[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 assert_eq!(nth(&tree, "p", 2).z_index, 0);
1779 assert_eq!(first(&tree, "section").z_index, 0);
1780 }
1781
1782 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 let both = page("@page { column-count: 2; column-width: 100pt; column-gap: 16pt }");
2129 assert_eq!(both.column_count(), 2);
2130
2131 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 #[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 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 assert_eq!(first(&tree, "h1").text_indent, 6.0);
2167 assert_eq!(first(&tree, "em").font_weight, 700);
2168
2169 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 #[test]
2188 fn author_css_overrides_the_built_in_sheet() {
2189 let book = sample();
2190 let tree = compile(&book, "* { font-size: 13pt } p { font-size: 12pt }");
2193 assert_eq!(first(&tree, "p").font_size, 12.0);
2194 let tree = compile(&book, "p { font-size: 12pt } p { font-size: 14pt }");
2196 assert_eq!(first(&tree, "p").font_size, 14.0);
2197 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 #[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 #[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 #[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 #[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 #[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 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 assert_eq!(tree.masters().len(), 10);
2357 }
2358
2359 #[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 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 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 #[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 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 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 #[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 #[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 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 #[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, ®istry);
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 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 #[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, ®istry);
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 assert_eq!(nth(&tree, "p", 1).margin.left, 0.0);
2837 }
2838
2839 #[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 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 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 #[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 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 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 #[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 #[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 #[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 assert_eq!(tree.style(link).content, Content::None);
3038 assert_eq!(tree.style(link).font_size, 9.0);
3039 }
3040
3041 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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, ®istry);
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 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 #[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 let tree = compile(&book, "em { font-style: normal }");
3404 assert_eq!(first(&tree, "strong").font_id, face(false, 700));
3405 }
3406
3407 #[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 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 #[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 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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 assert_eq!(
3736 tree.style(book.sections[0].id),
3737 &tree.styles()[tree.nodes()[1].style as usize]
3738 );
3739 }
3740}