Skip to main content

fleuron/style/sheet/
mod.rs

1//! Parsing: CSS text in, rules and diagnostics out.
2//!
3//! Nothing here matches or cascades. What it does decide is what the
4//! engine claims to understand: a declaration outside the novel
5//! subset does not become a rule, it becomes a warning naming the
6//! position it was written at, and the rest of the sheet parses on.
7//!
8//! The parsers have a file each: `declaration` holds the property
9//! table, `value` reads one value, `color` reads a colour, `page`
10//! reads `@page`, `face` reads `@font-face`, and `vocabulary` is what
11//! the subset document is written from.
12
13use std::fmt;
14use std::ops::Range;
15
16use cssparser::{
17    AtRuleParser, BasicParseErrorKind, CowRcStr, ParseError, ParseErrorKind, Parser, ParserInput,
18    ParserState, QualifiedRuleParser, RuleBodyParser, SourceLocation, StyleSheetParser,
19    match_ignore_ascii_case,
20};
21use selectors::SelectorList;
22use selectors::parser::{ParseRelative, SelectorParseErrorKind};
23
24use crate::Warning;
25use crate::pages::Side;
26use crate::style::element::{Fleuron, PseudoElement};
27use crate::style::properties::{
28    AlignContent, BackgroundRepeat, BorderStyle, Color, Content, Declaration, Edge, Length,
29    MarginBox, Pending, SizeSource, Url,
30};
31
32mod color;
33mod custom;
34mod declaration;
35mod face;
36mod page;
37mod value;
38mod vocabulary;
39
40pub use face::{FontFace, Src};
41
42pub(crate) use color::NAMED;
43pub(crate) use custom::{
44    Unresolved, longhands, margin_longhands, page_longhands, read_pending, read_pending_margin,
45    read_pending_page, substitute,
46};
47pub(crate) use declaration::{PROPERTIES, Spec};
48pub(crate) use face::FONT_FACE_DESCRIPTORS;
49#[cfg(test)]
50pub(crate) use face::FaceDeclaration;
51pub(crate) use page::{MARGIN_BOX_PROPERTIES, PAGE_PROPERTIES, PAGE_SELECTORS, PAGE_SIZES};
52pub(crate) use value::UNITS;
53pub(crate) use vocabulary::{
54    COMBINATORS, COMPOUNDS, CUSTOM_PROPERTY, DECLARATION, FIRST_LINE_PROPERTIES, PSEUDO_CLASSES,
55    PSEUDO_ELEMENTS, SELECTOR_LIST, VAR,
56};
57
58use declaration::declarations;
59use face::font_face;
60use page::{PageBody, PageItem, page_selector};
61
62/// Where a stylesheet came from. The cascade sorts by this before it
63/// sorts by anything else: author CSS overrides the built-in sheet
64/// however specific the built-in rule was.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
66pub enum Origin {
67    /// The built-in sheet.
68    UserAgent,
69    /// A sheet the host handed in.
70    #[default]
71    Author,
72}
73
74/// One stylesheet handed to the compiler: its text, a name for
75/// diagnostics, and the origin it cascades at.
76#[derive(Debug, Clone)]
77pub struct Source<'a> {
78    /// What diagnostics call this sheet.
79    pub name: &'a str,
80    /// The sheet's text.
81    pub css: &'a str,
82    /// Which origin it cascades at.
83    pub origin: Origin,
84}
85
86impl<'a> Source<'a> {
87    /// A sheet the author supplied.
88    pub fn author(name: &'a str, css: &'a str) -> Source<'a> {
89        Source {
90            name,
91            css,
92            origin: Origin::Author,
93        }
94    }
95
96    /// A sheet that cascades with the built-in defaults.
97    pub fn user_agent(name: &'a str, css: &'a str) -> Source<'a> {
98        Source {
99            name,
100            css,
101            origin: Origin::UserAgent,
102        }
103    }
104}
105
106/// `!important` beats a normal declaration of the same property in
107/// the same origin.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
109pub enum Importance {
110    Normal,
111    Important,
112}
113
114/// Where something was written in a stylesheet: the sheet's name, and
115/// the line and column of its first character, counted the way a
116/// warning counts them.
117#[derive(Debug, Clone, Default, PartialEq, Eq)]
118pub struct SheetPosition {
119    /// The name the sheet was handed in under.
120    pub sheet: String,
121    /// The line, counting from 1.
122    pub line: u32,
123    /// The column, counting from 1.
124    pub column: u32,
125}
126
127impl SheetPosition {
128    pub(super) fn at(sheet: &str, location: SourceLocation) -> SheetPosition {
129        SheetPosition {
130            sheet: sheet.to_string(),
131            line: location.line + 1,
132            column: location.column,
133        }
134    }
135}
136
137impl fmt::Display for SheetPosition {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(f, "{}:{}:{}", self.sheet, self.line, self.column)
140    }
141}
142
143/// One declaration as the author wrote it, and the longhands it
144/// became.
145#[derive(Debug, Clone, PartialEq)]
146pub struct Written {
147    /// The property name, in lowercase.
148    pub property: String,
149    /// The value, as written, without `!important`.
150    pub value: String,
151    /// Whether it was written `!important`.
152    pub important: bool,
153    /// Which of the rule's declarations it expanded to: several for a
154    /// shorthand, one for a longhand.
155    pub longhands: Range<usize>,
156    /// Where the declaration begins.
157    pub position: SheetPosition,
158}
159
160/// One style rule: what it matches, and what it says.
161#[derive(Debug)]
162pub struct StyleRule {
163    pub selectors: SelectorList<Fleuron>,
164    pub declarations: Vec<(Declaration, Importance)>,
165    /// The declarations as they were written, in order.
166    pub written: Vec<Written>,
167    /// Where the rule begins.
168    pub position: SheetPosition,
169}
170
171/// A `@page` rule: which pages it selects, the page box it sets, and
172/// the margin boxes it fills.
173#[derive(Debug, Default, Clone)]
174pub struct PageRule {
175    /// The named page this rule is for, from `page: <name>`.
176    pub name: Option<String>,
177    /// `:first` — the page a page group opens on.
178    pub first: bool,
179    /// `:blank` — a page inserted to square the sheet.
180    pub blank: bool,
181    /// `:left` / `:right`, as the side of the spread.
182    pub side: Option<Side>,
183    pub declarations: Vec<PageDeclaration>,
184    pub boxes: Vec<MarginRule>,
185    /// The prelude as written, `@page` included.
186    pub selector: String,
187    /// Where the rule begins.
188    pub position: SheetPosition,
189}
190
191/// One margin box inside a `@page` rule.
192#[derive(Debug, Clone)]
193pub struct MarginRule {
194    pub which: MarginBox,
195    pub declarations: Vec<MarginDeclaration>,
196    /// The declarations as they were written, in order.
197    pub written: Vec<Written>,
198}
199
200impl PageRule {
201    /// CSS 2.1 page-selector specificity: the name outweighs `:first`
202    /// and `:blank`, which outweigh `:left` and `:right`.
203    pub fn specificity(&self) -> (u8, u8, u8) {
204        (
205            self.name.is_some() as u8,
206            (self.first || self.blank) as u8,
207            self.side.is_some() as u8,
208        )
209    }
210}
211
212/// A declaration inside `@page`.
213#[derive(Debug, Clone, PartialEq)]
214pub enum PageDeclaration {
215    /// Trim size in points.
216    Size(f32, f32),
217    Margin(Edge, Length),
218    BackgroundColor(Option<Color>),
219    BackgroundImage(Option<Url>),
220    BackgroundRepeat(BackgroundRepeat),
221    BackgroundSize(SizeSource),
222    /// Across the page, then down it.
223    BackgroundPosition(Length, Length),
224    /// `column-count`, or `None` for `auto`.
225    ColumnCount(Option<u32>),
226    /// `column-width`, or `None` for `auto`.
227    ColumnWidth(Option<Length>),
228    /// `column-gap`, or `None` for `normal`.
229    ColumnGap(Option<Length>),
230    ColumnRuleWidth(Length),
231    ColumnRuleStyle(BorderStyle),
232    AlignContent(AlignContent),
233    /// A value that reads a custom property of the book's root.
234    Pending(Pending),
235}
236
237/// A declaration inside a page margin box.
238#[derive(Debug, Clone, PartialEq)]
239pub enum MarginDeclaration {
240    Content(Content),
241    /// A text property; margin boxes set a line like any other.
242    Style(Declaration),
243    /// A value that reads a custom property of the book's root.
244    Pending(Pending),
245}
246
247/// One parsed sheet.
248#[derive(Debug, Default)]
249pub struct Sheet {
250    pub origin: Origin,
251    pub rules: Vec<StyleRule>,
252    pub pages: Vec<PageRule>,
253    pub faces: Vec<FontFace>,
254}
255
256/// Why some fragment of CSS did not become a rule.
257#[derive(Debug, Clone)]
258pub enum StyleError<'i> {
259    UnsupportedProperty(CowRcStr<'i>),
260    NotOnFirstLine(CowRcStr<'i>),
261    UnsupportedValue(CowRcStr<'i>),
262    UnsupportedAtRule(CowRcStr<'i>),
263    UnsupportedPageSelector,
264    Selector(SelectorParseErrorKind<'i>),
265}
266
267impl<'i> From<SelectorParseErrorKind<'i>> for StyleError<'i> {
268    fn from(kind: SelectorParseErrorKind<'i>) -> Self {
269        StyleError::Selector(kind)
270    }
271}
272
273/// Parses one sheet, returning what the engine understood and a
274/// warning for everything else.
275pub fn parse(source: &Source<'_>) -> (Sheet, Vec<Warning>) {
276    let mut input = ParserInput::new(source.css);
277    let mut parser = Parser::new(&mut input);
278    let mut top = TopLevel {
279        sheet: Sheet {
280            origin: source.origin,
281            ..Sheet::default()
282        },
283        warnings: Vec::new(),
284        name: source.name.to_string(),
285    };
286    let rules = StyleSheetParser::new(&mut parser, &mut top);
287    let collected: Vec<_> = rules
288        .map(|result| result.map_err(|(error, _)| error))
289        .collect();
290    for result in collected {
291        match result {
292            Ok(rule) => top.keep(rule),
293            Err(error) => top.warn(&error),
294        }
295    }
296    (top.sheet, top.warnings)
297}
298
299/// A parsed top-level rule, before it is filed into the sheet.
300pub enum Rule {
301    Style(StyleRule),
302    Page(PageRule),
303    FontFace(FontFace),
304}
305
306struct TopLevel {
307    sheet: Sheet,
308    warnings: Vec<Warning>,
309    name: String,
310}
311
312impl TopLevel {
313    fn keep(&mut self, rule: Rule) {
314        match rule {
315            Rule::Style(rule) => self.sheet.rules.push(rule),
316            Rule::Page(rule) => self.sheet.pages.push(rule),
317            Rule::FontFace(face) => self.sheet.faces.push(face),
318        }
319    }
320
321    fn warn(&mut self, error: &ParseError<'_, StyleError<'_>>) {
322        self.warnings.push(warning(&self.name, error));
323    }
324}
325
326/// What happens to a declaration the parser refuses.
327const IGNORED_DECLARATION: &str = "The declaration is ignored.";
328
329/// What happens to a rule the parser refuses.
330const IGNORED_RULE: &str = "The rule is ignored.";
331
332/// One parse error as the diagnostic a reader can act on: what the
333/// engine did not understand, and where it was written.
334pub fn warning(sheet: &str, error: &ParseError<'_, StyleError<'_>>) -> Warning {
335    let message = match &error.kind {
336        ParseErrorKind::Custom(StyleError::UnsupportedProperty(name)) => {
337            format!("Unsupported property `{name}`. {IGNORED_DECLARATION}")
338        }
339        ParseErrorKind::Custom(StyleError::NotOnFirstLine(name)) => {
340            format!("Unsupported property `{name}` on `::first-line`. {IGNORED_DECLARATION}")
341        }
342        ParseErrorKind::Custom(StyleError::UnsupportedValue(name)) => {
343            format!("Unsupported value for `{name}`. {IGNORED_DECLARATION}")
344        }
345        ParseErrorKind::Custom(StyleError::UnsupportedAtRule(name)) => {
346            format!("Unsupported at-rule `@{name}`. {IGNORED_RULE}")
347        }
348        ParseErrorKind::Custom(StyleError::UnsupportedPageSelector) => {
349            format!("Unsupported `@page` selector. {IGNORED_RULE}")
350        }
351        ParseErrorKind::Custom(StyleError::Selector(
352            SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
353        )) => format!("Unsupported selector `:{name}`. {IGNORED_RULE}"),
354        ParseErrorKind::Custom(StyleError::Selector(_)) => {
355            format!("Unsupported selector. {IGNORED_RULE}")
356        }
357        ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(name)) => {
358            format!("Unsupported at-rule `@{name}`. {IGNORED_RULE}")
359        }
360        ParseErrorKind::Basic(_) => format!("Malformed CSS. {IGNORED_DECLARATION}"),
361    };
362    Warning {
363        message,
364        origin: Some(position(sheet, error.location)),
365    }
366}
367
368/// A CSS position as diagnostics spell it: `author.css:12:3`.
369pub(super) fn position(sheet: &str, location: SourceLocation) -> String {
370    SheetPosition::at(sheet, location).to_string()
371}
372
373impl<'i> QualifiedRuleParser<'i> for TopLevel {
374    type Prelude = SelectorList<Fleuron>;
375    type QualifiedRule = Rule;
376    type Error = StyleError<'i>;
377
378    fn parse_prelude<'t>(
379        &mut self,
380        input: &mut Parser<'i, 't>,
381    ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
382        SelectorList::parse(&Selectors, input, ParseRelative::No)
383    }
384
385    fn parse_block<'t>(
386        &mut self,
387        selectors: Self::Prelude,
388        start: &ParserState,
389        input: &mut Parser<'i, 't>,
390    ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
391        let first_line = selectors
392            .slice()
393            .iter()
394            .all(|selector| selector.pseudo_element() == Some(&PseudoElement::FirstLine));
395        let (declarations, written, warnings) = declarations(input, &self.name, first_line);
396        self.warnings.extend(warnings);
397        Ok(Rule::Style(StyleRule {
398            selectors,
399            declarations,
400            written,
401            position: SheetPosition::at(&self.name, start.source_location()),
402        }))
403    }
404}
405
406impl<'i> AtRuleParser<'i> for TopLevel {
407    type Prelude = AtRule;
408    type AtRule = Rule;
409    type Error = StyleError<'i>;
410
411    fn parse_prelude<'t>(
412        &mut self,
413        name: CowRcStr<'i>,
414        input: &mut Parser<'i, 't>,
415    ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
416        match_ignore_ascii_case! { &name,
417            "page" => {
418                let from = input.position();
419                let mut rule = page_selector(input)?;
420                rule.selector = format!("@page {}", input.slice_from(from).trim())
421                    .trim_end()
422                    .to_string();
423                Ok(AtRule::Page(rule))
424            },
425            "font-face" => Ok(AtRule::FontFace),
426            _ => Err(input.new_custom_error(StyleError::UnsupportedAtRule(name.clone()))),
427        }
428    }
429
430    fn parse_block<'t>(
431        &mut self,
432        prelude: Self::Prelude,
433        start: &ParserState,
434        input: &mut Parser<'i, 't>,
435    ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> {
436        match prelude {
437            AtRule::Page(mut rule) => {
438                rule.position = SheetPosition::at(&self.name, start.source_location());
439                let mut body = PageBody {
440                    name: self.name.clone(),
441                    warnings: Vec::new(),
442                };
443                let items: Vec<_> = RuleBodyParser::new(input, &mut body)
444                    .map(|result| result.map_err(|(error, _)| error))
445                    .collect();
446                self.warnings.append(&mut body.warnings);
447                for item in items {
448                    match item {
449                        Ok(parsed) => {
450                            for item in parsed {
451                                match item {
452                                    PageItem::Declaration(declaration) => {
453                                        rule.declarations.push(declaration)
454                                    }
455                                    PageItem::Box(margin) => rule.boxes.push(margin),
456                                }
457                            }
458                        }
459                        Err(error) => self.warnings.push(warning(&self.name, &error)),
460                    }
461                }
462                Ok(Rule::Page(rule))
463            }
464            AtRule::FontFace => {
465                let (face, warnings) = font_face(input, &self.name);
466                self.warnings.extend(warnings);
467                Ok(Rule::FontFace(face))
468            }
469        }
470    }
471}
472
473/// The at-rules the engine parses, once their prelude is read.
474pub enum AtRule {
475    Page(PageRule),
476    FontFace,
477}
478
479/// The selector parser: the engine takes plain selectors and nothing
480/// that assumes a live document.
481struct Selectors;
482
483impl<'i> selectors::Parser<'i> for Selectors {
484    type Impl = Fleuron;
485    type Error = StyleError<'i>;
486
487    fn parse_is_and_where(&self) -> bool {
488        true
489    }
490
491    fn parse_nth_child_of(&self) -> bool {
492        true
493    }
494
495    fn parse_has(&self) -> bool {
496        true
497    }
498
499    fn parse_pseudo_element(
500        &self,
501        location: SourceLocation,
502        name: CowRcStr<'i>,
503    ) -> Result<PseudoElement, ParseError<'i, StyleError<'i>>> {
504        match_ignore_ascii_case! { &name,
505            "first-letter" => Ok(PseudoElement::FirstLetter),
506            "first-line" => Ok(PseudoElement::FirstLine),
507            "before" => Ok(PseudoElement::Before),
508            "after" => Ok(PseudoElement::After),
509            _ => Err(location.new_custom_error(
510                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()),
511            )),
512        }
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    /// A rule records the sheet, line and column it begins at, and a
521    /// warning about a rule written at the same place names the same
522    /// position.
523    #[test]
524    fn a_rule_records_the_position_a_warning_names() {
525        let kept = "p { color: red }\n\n  h1 { color: red }";
526        let refused = "p { color: red }\n\n  !h1 { color: red }";
527        let (sheet, _) = parse(&Source::author("author.css", kept));
528        let (_, warnings) = parse(&Source::author("author.css", refused));
529
530        let rule = &sheet.rules[1];
531        assert_eq!(
532            rule.position,
533            SheetPosition {
534                sheet: "author.css".into(),
535                line: 3,
536                column: 3,
537            }
538        );
539        assert_eq!(warnings.len(), 1, "{warnings:?}");
540        assert_eq!(
541            warnings[0].origin.as_deref(),
542            Some(rule.position.to_string().as_str())
543        );
544
545        let page = "\n@page :left {\n  @top-left { content: \"Left\" }\n}";
546        let (sheet, _) = parse(&Source::author("pages.css", page));
547        assert_eq!(sheet.pages[0].position.to_string(), "pages.css:2:1");
548        assert_eq!(sheet.pages[0].selector, "@page :left");
549    }
550
551    /// A declaration keeps the name and value it was written with,
552    /// and a shorthand names every longhand it became.
553    #[test]
554    fn a_declaration_keeps_what_was_written() {
555        let css = "p { MARGIN: 1em 2em; color: red !important }";
556        let (sheet, _) = parse(&Source::author("author.css", css));
557        let rule = &sheet.rules[0];
558        assert_eq!(
559            rule.written,
560            vec![
561                Written {
562                    property: "margin".into(),
563                    value: "1em 2em".into(),
564                    important: false,
565                    longhands: 0..4,
566                    position: SheetPosition {
567                        sheet: "author.css".into(),
568                        line: 1,
569                        column: 5,
570                    },
571                },
572                Written {
573                    property: "color".into(),
574                    value: "red".into(),
575                    important: true,
576                    longhands: 4..5,
577                    position: SheetPosition {
578                        sheet: "author.css".into(),
579                        line: 1,
580                        column: 22,
581                    },
582                },
583            ]
584        );
585        assert_eq!(rule.declarations.len(), 5);
586    }
587
588    /// Part: a declaration whose name starts with `--` is kept as the
589    /// text it was written as rather than rejected, and a value that
590    /// reads one is kept as text for the cascade.
591    #[test]
592    fn a_custom_property_is_kept_as_written() {
593        let css = "p {\n  --Accent: rgb(214, 7, 94) !important;\n  color: var(--Accent, black);\n}";
594        let (sheet, warnings) = parse(&Source::author("author.css", css));
595        assert!(warnings.is_empty(), "{warnings:?}");
596        let rule = &sheet.rules[0];
597        assert_eq!(
598            rule.declarations,
599            vec![
600                (
601                    Declaration::Custom(crate::style::properties::Custom {
602                        name: "--Accent".into(),
603                        value: "rgb(214, 7, 94)".into(),
604                        origin: "author.css:2:3".into(),
605                    }),
606                    Importance::Important,
607                ),
608                (
609                    Declaration::Pending(Pending {
610                        property: "color".into(),
611                        value: "var(--Accent, black)".into(),
612                        origin: "author.css:3:3".into(),
613                    }),
614                    Importance::Normal,
615                ),
616            ]
617        );
618        assert_eq!(rule.written[0].property, "--Accent");
619        assert_eq!(rule.written[1].value, "var(--Accent, black)");
620    }
621}