hypertext_garnish/
css.rs

1use serde::Deserialize;
2
3#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
4pub enum DeclarationValue {
5    Basic(String),
6    Function(String, Vec<String>), // (function name, function arguments
7}
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),                                          // tag name
51    Class(String),                                        // class name
52    Id(String),                                           // id name
53    Combinator(Box<Selector>, Combinator, Box<Selector>), // (base selector, combination)
54    PseudoClass(Box<Selector>, String),                   // (base selector, pseudo class)
55    PseudoElement(Box<Selector>, String),                 // (base selector, pseudo element)
56    Attribute(String),                                    // attribute name
57    AttributeValue(String, String),                       // (attribute name, attribute value)
58    AttributeContains(String, String),                    // (attribute name, search string)
59    Chain(Vec<Selector>), // no space merge (e.g. p.my-class[someAttribute])
60    Group(Vec<Selector>), // comma separated list (e.g. body, h1, p)
61}
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}