1use serde::Deserialize;
2
3#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
4pub enum DeclarationValue {
5 Basic(String),
6 Function(String, Vec<String>), }
8
9impl ToString for DeclarationValue {
10 fn to_string(&self) -> String {
11 match self {
12 DeclarationValue::Basic(s) => match s.contains(" ") {
13 true => format!("\"{}\"", s),
14 false => s.to_string(),
15 },
16 DeclarationValue::Function(name, args) => format!("{}({})", name, args.join(",")),
17 }
18 }
19}
20
21#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
22pub struct Declaration {
23 property: String,
24 value: DeclarationValue,
25}
26
27impl Declaration {
28 pub fn new(property: String, value: DeclarationValue) -> Self {
29 Self { property, value }
30 }
31}
32
33impl ToString for Declaration {
34 fn to_string(&self) -> String {
35 format!("{}:{};", self.property, self.value.to_string())
36 }
37}
38
39#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
40pub enum Combinator {
41 Descendant,
42 Child,
43 AdjacentSibling,
44 GeneralSibling,
45}
46
47#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
48pub enum Selector {
49 Universal,
50 Tag(String), Class(String), Id(String), Combinator(Box<Selector>, Combinator, Box<Selector>), PseudoClass(Box<Selector>, String), PseudoElement(Box<Selector>, String), Attribute(String), AttributeValue(String, String), AttributeContains(String, String), Chain(Vec<Selector>), Group(Vec<Selector>), }
62
63impl ToString for Selector {
64 fn to_string(&self) -> String {
65 match self {
66 Selector::Universal => "*".to_string(),
67 Selector::Tag(s) => s.to_string(),
68 Selector::Id(id) => format!("#{}", id),
69 Selector::Class(class) => format!(".{}", class),
70 Selector::Combinator(base, op, relative) => {
71 format!(
72 "{}{}{}",
73 base.to_string(),
74 match op {
75 Combinator::Descendant => " ",
76 Combinator::Child => ">",
77 Combinator::AdjacentSibling => "+",
78 Combinator::GeneralSibling => "~",
79 },
80 relative.to_string()
81 )
82 }
83 Selector::PseudoClass(base, class) => format!("{}:{}", base.to_string(), class),
84 Selector::PseudoElement(base, class) => format!("{}::{}", base.to_string(), class),
85 Selector::Attribute(attr) => format!("[{}]", attr),
86 Selector::AttributeValue(attr, value) => format!("[{}=\"{}\"]", attr, value),
87 Selector::AttributeContains(attr, value) => format!("[{}~=\"{}\"]", attr, value),
88 Selector::Chain(items) => items
89 .iter()
90 .map(Selector::to_string)
91 .collect::<Vec<String>>()
92 .join(""),
93 Selector::Group(items) => items
94 .iter()
95 .map(Selector::to_string)
96 .collect::<Vec<String>>()
97 .join(","),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
103pub struct Rule {
104 selector: Selector,
105 declarations: Vec<Declaration>,
106 #[serde(default)]
107 sub_rules: Vec<Rule>,
108}
109
110impl Rule {
111 pub fn new(selector: Selector, declarations: Vec<Declaration>, sub_rules: Vec<Rule>) -> Self {
112 Self {
113 selector,
114 declarations,
115 sub_rules,
116 }
117 }
118
119 fn make_string(&self) -> String {
120 let mut all_rules = vec![format!(
121 "{}{{{}}}",
122 self.selector.to_string(),
123 self.declarations
124 .iter()
125 .map(Declaration::to_string)
126 .collect::<Vec<String>>()
127 .join("")
128 )];
129
130 let mut sub_rules = vec![(format!("{}>", self.selector.to_string()), &self.sub_rules)];
131
132 while let Some((prefix, rules)) = sub_rules.pop() {
133 for rule in rules {
134 all_rules.push(format!(
135 "{}{}{{{}}}",
136 prefix,
137 rule.selector.to_string(),
138 rule.declarations
139 .iter()
140 .map(Declaration::to_string)
141 .collect::<Vec<String>>()
142 .join("")
143 ));
144
145 if !rule.sub_rules.is_empty() {
146 sub_rules.push((
147 format!("{}{}>", prefix, rule.selector.to_string()),
148 &rule.sub_rules,
149 ))
150 }
151 }
152 }
153
154 all_rules.join("")
155 }
156}
157
158impl ToString for Rule {
159 fn to_string(&self) -> String {
160 self.make_string()
161 }
162}
163
164#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
165pub enum MediaConstraint {
166 None,
167 Not,
168 Only,
169}
170
171impl Default for MediaConstraint {
172 fn default() -> Self {
173 Self::None
174 }
175}
176
177#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
178pub struct MediaFeature {
179 property: String,
180 value: String,
181}
182
183impl MediaFeature {
184 pub fn new(property: String, value: String) -> Self {
185 Self { property, value }
186 }
187}
188
189impl ToString for MediaFeature {
190 fn to_string(&self) -> String {
191 format!("({}:{})", self.property, self.value)
192 }
193}
194
195#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
196pub enum MediaCondition {
197 Lone(MediaFeature),
198 And(MediaFeature, MediaFeature),
199 Or(MediaFeature, MediaFeature),
200 Not(MediaFeature, MediaFeature),
201}
202
203impl ToString for MediaCondition {
204 fn to_string(&self) -> String {
205 match self {
206 MediaCondition::Lone(f) => f.to_string(),
207 MediaCondition::And(f1, f2) => format!("{} and {}", f1.to_string(), f2.to_string()),
208 MediaCondition::Or(f1, f2) => format!("{} or {}", f1.to_string(), f2.to_string()),
209 MediaCondition::Not(f1, f2) => format!("{} not {}", f1.to_string(), f2.to_string())
210 }
211 }
212}
213
214#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
215pub struct MediaQuery {
216 media_type: String,
217 #[serde(default)]
218 constraint: MediaConstraint,
219 #[serde(default)]
220 features: Vec<MediaCondition>,
221}
222
223impl MediaQuery {
224 pub fn new(
225 constraint: MediaConstraint,
226 media_type: String,
227 features: Vec<MediaCondition>,
228 ) -> Self {
229 Self {
230 media_type,
231 constraint,
232 features,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
238pub struct RuleSet {
239 media_query: Option<MediaQuery>,
240 rules: Vec<Rule>,
241 #[serde(default)]
242 sub_sets: Vec<RuleSet>,
243}
244
245impl RuleSet {
246 pub fn new(rules: Vec<Rule>, sub_sets: Vec<RuleSet>, media_query: Option<MediaQuery>) -> Self {
247 Self {
248 rules,
249 sub_sets,
250 media_query,
251 }
252 }
253}
254
255impl ToString for RuleSet {
256 fn to_string(&self) -> String {
257 let all_sets = format!(
258 "{}{}",
259 self.rules
260 .iter()
261 .map(Rule::to_string)
262 .collect::<Vec<String>>()
263 .join(""),
264 self.sub_sets
265 .iter()
266 .map(RuleSet::to_string)
267 .collect::<Vec<String>>()
268 .join(""),
269 );
270
271 match &self.media_query {
272 None => all_sets,
273 Some(query) => format!(
274 "@media {}{}{}{{{}}}",
275 match query.constraint {
276 MediaConstraint::None => "",
277 MediaConstraint::Only => "only ",
278 MediaConstraint::Not => "not ",
279 },
280 query.media_type,
281 match query.features.len() {
282 0 => String::new(),
283 _ => format!(
284 " and {}",
285 query
286 .features
287 .iter()
288 .map(MediaCondition::to_string)
289 .collect::<Vec<String>>()
290 .join("")
291 ),
292 },
293 all_sets
294 ),
295 }
296 }
297}
298
299#[cfg(test)]
300mod to_string {
301 use crate::css::{
302 Combinator, Declaration, DeclarationValue, MediaCondition, MediaConstraint, MediaFeature,
303 MediaQuery, Rule, RuleSet, Selector,
304 };
305
306 #[test]
307 fn declaration() {
308 let d = Declaration::new(
309 "color".to_string(),
310 DeclarationValue::Basic("blue".to_string()),
311 );
312 assert_eq!(d.to_string(), "color:blue;")
313 }
314
315 #[test]
316 fn declaration_basic_quotes_strings_with_spaces() {
317 let d = Declaration::new(
318 "font-family".to_string(),
319 DeclarationValue::Basic("Times New Roman".to_string()),
320 );
321 assert_eq!(d.to_string(), "font-family:\"Times New Roman\";")
322 }
323
324 #[test]
325 fn declaration_with_function() {
326 let d = Declaration::new(
327 "color".to_string(),
328 DeclarationValue::Function(
329 "rgb".to_string(),
330 vec!["200".into(), "200".into(), "200".into()],
331 ),
332 );
333 assert_eq!(d.to_string(), "color:rgb(200,200,200);")
334 }
335
336 #[test]
337 fn universal_selector() {
338 let s = Selector::Universal;
339
340 assert_eq!(s.to_string(), "*");
341 }
342
343 #[test]
344 fn tag_selector() {
345 let s = Selector::Tag("body".to_string());
346
347 assert_eq!(s.to_string(), "body");
348 }
349
350 #[test]
351 fn class_selector() {
352 let s = Selector::Class("my-class".to_string());
353
354 assert_eq!(s.to_string(), ".my-class");
355 }
356
357 #[test]
358 fn id_selector() {
359 let s = Selector::Id("my_id".to_string());
360
361 assert_eq!(s.to_string(), "#my_id");
362 }
363
364 #[test]
365 fn combinator_descendant() {
366 let s = Selector::Combinator(
367 Box::new(Selector::Tag("body".to_string())),
368 Combinator::Descendant,
369 Box::new(Selector::Tag("h1".to_string())),
370 );
371
372 assert_eq!(s.to_string(), "body h1");
373 }
374
375 #[test]
376 fn combinator_child() {
377 let s = Selector::Combinator(
378 Box::new(Selector::Tag("body".to_string())),
379 Combinator::Child,
380 Box::new(Selector::Tag("h1".to_string())),
381 );
382
383 assert_eq!(s.to_string(), "body>h1");
384 }
385
386 #[test]
387 fn combinator_adjacent_sibling() {
388 let s = Selector::Combinator(
389 Box::new(Selector::Tag("body".to_string())),
390 Combinator::AdjacentSibling,
391 Box::new(Selector::Tag("h1".to_string())),
392 );
393
394 assert_eq!(s.to_string(), "body+h1");
395 }
396
397 #[test]
398 fn combinator_general_sibling() {
399 let s = Selector::Combinator(
400 Box::new(Selector::Tag("body".to_string())),
401 Combinator::GeneralSibling,
402 Box::new(Selector::Tag("h1".to_string())),
403 );
404
405 assert_eq!(s.to_string(), "body~h1");
406 }
407
408 #[test]
409 fn combinator_multiple() {
410 let s = Selector::Combinator(
411 Box::new(Selector::Combinator(
412 Box::new(Selector::Tag("body".to_string())),
413 Combinator::Child,
414 Box::new(Selector::Tag("section".to_string())),
415 )),
416 Combinator::GeneralSibling,
417 Box::new(Selector::Tag("h1".to_string())),
418 );
419
420 assert_eq!(s.to_string(), "body>section~h1");
421 }
422
423 #[test]
424 fn pseudo_class() {
425 let s = Selector::PseudoClass(
426 Box::new(Selector::Tag("body".to_string())),
427 "hover".to_string(),
428 );
429
430 assert_eq!(s.to_string(), "body:hover");
431 }
432
433 #[test]
434 fn pseudo_element() {
435 let s = Selector::PseudoElement(
436 Box::new(Selector::Tag("body".to_string())),
437 "first-line".to_string(),
438 );
439
440 assert_eq!(s.to_string(), "body::first-line");
441 }
442
443 #[test]
444 fn attribute() {
445 let s = Selector::Attribute("title".to_string());
446
447 assert_eq!(s.to_string(), "[title]");
448 }
449
450 #[test]
451 fn attribute_value() {
452 let s = Selector::AttributeValue("title".to_string(), "hello".to_string());
453
454 assert_eq!(s.to_string(), "[title=\"hello\"]");
455 }
456
457 #[test]
458 fn attribute_contains() {
459 let s = Selector::AttributeContains("title".to_string(), "hello".to_string());
460
461 assert_eq!(s.to_string(), "[title~=\"hello\"]");
462 }
463
464 #[test]
465 fn chain() {
466 let s = Selector::Chain(vec![
467 Selector::Tag("body".to_string()),
468 Selector::Class("main".to_string()),
469 Selector::Attribute("title".to_string()),
470 ]);
471
472 assert_eq!(s.to_string(), "body.main[title]");
473 }
474
475 #[test]
476 fn group() {
477 let s = Selector::Group(vec![
478 Selector::Tag("body".to_string()),
479 Selector::Class("main".to_string()),
480 Selector::Id("title".to_string()),
481 ]);
482
483 assert_eq!(s.to_string(), "body,.main,#title");
484 }
485
486 #[test]
487 fn rule() {
488 let rule = Rule::new(
489 Selector::Tag("body".to_string()),
490 vec![
491 Declaration::new(
492 "color".to_string(),
493 DeclarationValue::Basic("blue".to_string()),
494 ),
495 Declaration::new(
496 "background-color".to_string(),
497 DeclarationValue::Basic("red".to_string()),
498 ),
499 Declaration::new(
500 "font-family".to_string(),
501 DeclarationValue::Basic("Times New Roman".to_string()),
502 ),
503 ],
504 vec![],
505 );
506
507 assert_eq!(
508 rule.to_string(),
509 "body{color:blue;background-color:red;font-family:\"Times New Roman\";}"
510 )
511 }
512
513 #[test]
514 fn rule_with_sub_rules() {
515 let rule = Rule::new(
516 Selector::Tag("body".to_string()),
517 vec![Declaration::new(
518 "color".to_string(),
519 DeclarationValue::Basic("blue".to_string()),
520 )],
521 vec![Rule::new(
522 Selector::Tag("section".to_string()),
523 vec![Declaration::new(
524 "background-color".to_string(),
525 DeclarationValue::Basic("red".to_string()),
526 )],
527 vec![Rule::new(
528 Selector::Tag("h1".to_string()),
529 vec![Declaration::new(
530 "font-family".to_string(),
531 DeclarationValue::Basic("Times New Roman".to_string()),
532 )],
533 vec![],
534 )],
535 )],
536 );
537
538 assert_eq!(
539 rule.to_string(),
540 "body{color:blue;}body>section{background-color:red;}body>section>h1{font-family:\"Times New Roman\";}"
541 )
542 }
543
544 fn make_rule_set() -> RuleSet {
545 RuleSet::new(
546 vec![
547 Rule::new(
548 Selector::Tag("body".to_string()),
549 vec![Declaration::new(
550 "color".to_string(),
551 DeclarationValue::Basic("blue".to_string()),
552 )],
553 vec![],
554 ),
555 Rule::new(
556 Selector::Tag("section".to_string()),
557 vec![Declaration::new(
558 "background-color".to_string(),
559 DeclarationValue::Basic("red".to_string()),
560 )],
561 vec![],
562 ),
563 Rule::new(
564 Selector::Tag("h1".to_string()),
565 vec![Declaration::new(
566 "font-family".to_string(),
567 DeclarationValue::Basic("Times New Roman".to_string()),
568 )],
569 vec![],
570 ),
571 ],
572 vec![],
573 None,
574 )
575 }
576
577 #[test]
578 fn rule_set() {
579 let set = make_rule_set();
580
581 assert_eq!(
582 set.to_string(),
583 "body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}"
584 )
585 }
586
587 #[test]
588 fn rule_set_with_query() {
589 let mut set = make_rule_set();
590 set.media_query = Some(MediaQuery::new(
591 MediaConstraint::None,
592 "screen".to_string(),
593 vec![],
594 ));
595
596 assert_eq!(
597 set.to_string(),
598 "@media screen{body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
599 )
600 }
601
602 #[test]
603 fn rule_set_with_query_constraint_only() {
604 let mut set = make_rule_set();
605 set.media_query = Some(MediaQuery::new(
606 MediaConstraint::Only,
607 "screen".to_string(),
608 vec![],
609 ));
610
611 assert_eq!(
612 set.to_string(),
613 "@media only screen{body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
614 )
615 }
616
617 #[test]
618 fn rule_set_with_query_constraint_not() {
619 let mut set = make_rule_set();
620 set.media_query = Some(MediaQuery::new(
621 MediaConstraint::Not,
622 "screen".to_string(),
623 vec![],
624 ));
625
626 assert_eq!(
627 set.to_string(),
628 "@media not screen{body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
629 )
630 }
631
632 #[test]
633 fn rule_set_with_query_with_feature() {
634 let mut set = make_rule_set();
635 set.media_query = Some(MediaQuery::new(
636 MediaConstraint::None,
637 "screen".to_string(),
638 vec![MediaCondition::Lone(MediaFeature::new(
639 "max-width".to_string(),
640 "1000px".to_string(),
641 ))],
642 ));
643
644 assert_eq!(
645 set.to_string(),
646 "@media screen and (max-width:1000px){body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
647 )
648 }
649
650 #[test]
651 fn rule_set_with_query_with_and_feature() {
652 let mut set = make_rule_set();
653 set.media_query = Some(MediaQuery::new(
654 MediaConstraint::None,
655 "screen".to_string(),
656 vec![MediaCondition::And(
657 MediaFeature::new("max-width".to_string(), "1000px".to_string()),
658 MediaFeature::new("orientation".to_string(), "landscape".to_string()),
659 )],
660 ));
661
662 assert_eq!(
663 set.to_string(),
664 "@media screen and (max-width:1000px) and (orientation:landscape){body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
665 )
666 }
667
668 #[test]
669 fn rule_set_with_query_with_or_feature() {
670 let mut set = make_rule_set();
671 set.media_query = Some(MediaQuery::new(
672 MediaConstraint::None,
673 "screen".to_string(),
674 vec![MediaCondition::Or(
675 MediaFeature::new("max-width".to_string(), "1000px".to_string()),
676 MediaFeature::new("orientation".to_string(), "landscape".to_string()),
677 )],
678 ));
679
680 assert_eq!(
681 set.to_string(),
682 "@media screen and (max-width:1000px) or (orientation:landscape){body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
683 )
684 }
685
686 #[test]
687 fn rule_set_with_query_with_not_feature() {
688 let mut set = make_rule_set();
689 set.media_query = Some(MediaQuery::new(
690 MediaConstraint::None,
691 "screen".to_string(),
692 vec![MediaCondition::Not(
693 MediaFeature::new("max-width".to_string(), "1000px".to_string()),
694 MediaFeature::new("orientation".to_string(), "landscape".to_string()),
695 )],
696 ));
697
698 assert_eq!(
699 set.to_string(),
700 "@media screen and (max-width:1000px) not (orientation:landscape){body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}"
701 )
702 }
703
704 #[test]
705 fn rule_set_multiple_no_media_query_dont_nest() {
706 let mut set = make_rule_set();
707 set.sub_sets.push(make_rule_set());
708
709 assert_eq!(set.to_string(), "body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}")
710 }
711
712 #[test]
713 fn rule_set_multiple_with_media_query() {
714 let mut set = make_rule_set();
715 let mut with_media = make_rule_set();
716 with_media.media_query = Some(MediaQuery::new(
717 MediaConstraint::None,
718 "screen".to_string(),
719 vec![],
720 ));
721 set.sub_sets.push(with_media);
722
723 assert_eq!(set.to_string(), "body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}@media screen{body{color:blue;}section{background-color:red;}h1{font-family:\"Times New Roman\";}}")
724 }
725}