1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
66pub enum Origin {
67 UserAgent,
69 #[default]
71 Author,
72}
73
74#[derive(Debug, Clone)]
77pub struct Source<'a> {
78 pub name: &'a str,
80 pub css: &'a str,
82 pub origin: Origin,
84}
85
86impl<'a> Source<'a> {
87 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
109pub enum Importance {
110 Normal,
111 Important,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
118pub struct SheetPosition {
119 pub sheet: String,
121 pub line: u32,
123 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#[derive(Debug, Clone, PartialEq)]
146pub struct Written {
147 pub property: String,
149 pub value: String,
151 pub important: bool,
153 pub longhands: Range<usize>,
156 pub position: SheetPosition,
158}
159
160#[derive(Debug)]
162pub struct StyleRule {
163 pub selectors: SelectorList<Fleuron>,
164 pub declarations: Vec<(Declaration, Importance)>,
165 pub written: Vec<Written>,
167 pub position: SheetPosition,
169}
170
171#[derive(Debug, Default, Clone)]
174pub struct PageRule {
175 pub name: Option<String>,
177 pub first: bool,
179 pub blank: bool,
181 pub side: Option<Side>,
183 pub declarations: Vec<PageDeclaration>,
184 pub boxes: Vec<MarginRule>,
185 pub selector: String,
187 pub position: SheetPosition,
189}
190
191#[derive(Debug, Clone)]
193pub struct MarginRule {
194 pub which: MarginBox,
195 pub declarations: Vec<MarginDeclaration>,
196 pub written: Vec<Written>,
198}
199
200impl PageRule {
201 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#[derive(Debug, Clone, PartialEq)]
214pub enum PageDeclaration {
215 Size(f32, f32),
217 Margin(Edge, Length),
218 BackgroundColor(Option<Color>),
219 BackgroundImage(Option<Url>),
220 BackgroundRepeat(BackgroundRepeat),
221 BackgroundSize(SizeSource),
222 BackgroundPosition(Length, Length),
224 ColumnCount(Option<u32>),
226 ColumnWidth(Option<Length>),
228 ColumnGap(Option<Length>),
230 ColumnRuleWidth(Length),
231 ColumnRuleStyle(BorderStyle),
232 AlignContent(AlignContent),
233 Pending(Pending),
235}
236
237#[derive(Debug, Clone, PartialEq)]
239pub enum MarginDeclaration {
240 Content(Content),
241 Style(Declaration),
243 Pending(Pending),
245}
246
247#[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#[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
273pub 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
299pub 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
326const IGNORED_DECLARATION: &str = "The declaration is ignored.";
328
329const IGNORED_RULE: &str = "The rule is ignored.";
331
332pub 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
368pub(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
473pub enum AtRule {
475 Page(PageRule),
476 FontFace,
477}
478
479struct 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 #[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 #[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 #[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}