1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#![feature(let_chains)]
#![feature(box_patterns)]
#![feature(iterator_try_reduce)]
#![feature(assert_matches)]

mod directive;
mod eval;
mod expression;
mod literal;
mod plugin;
mod subject;

pub type NomSpan<'a> = LocatedSpan<&'a str, Span>;

use nom_locate::LocatedSpan;
use swc_core::common::Span;

pub use directive::*;
pub use expression::*;
pub use literal::*;
pub use plugin::*;
pub use subject::*;

#[cfg(test)]
mod test {
    use std::assert_matches::assert_matches;

    use crate::{
        Border, Css, Directive, Display, Expression, Grid, Literal, Max, Plugin, Position, Subject,
        SubjectValue, TextDecoration, Value,
    };

    use itertools::Itertools;
    use nom_locate::LocatedSpan;
    use stailwc_swc_utils::sort_recursive;
    use swc_core::common::DUMMY_SP;
    use tailwind_config::{LineHeightOpt, TailwindConfig};
    use test_case::test_case;

    use pretty_assertions::assert_eq;

    #[test]
    fn directive() -> anyhow::Result<()> {
        let (rest, _d, _errs) = Directive::parse(LocatedSpan::new_extra(
            "-h-4 md:bg-blue text-white! hover:(text-blue bg-white lg:text-black!)",
            DUMMY_SP,
        ));

        assert!(rest.len() == 0);

        Ok(())
    }

    #[test]
    fn recovery() {
        let (rest, _d, errs) =
            Directive::parse(LocatedSpan::new_extra(" fail text-white", DUMMY_SP));

        assert!(rest.len() == 0);
        assert_eq!(errs.len(), 1);
    }

    #[test]
    fn recovery2() {
        let (rest, _d, errs) =
            Directive::parse(LocatedSpan::new_extra("sm:max--3xl smpx-6", DUMMY_SP));

        assert!(rest.len() == 0);
        assert_eq!(errs.len(), 2);
    }

    #[test_case("flex!", None, None, true, false ; "important")]
    #[test_case("underline!", None, None, true, false ; "important with transparent command")]
    #[test_case("min-w-4!", Some(SubjectValue::Value(Value("4"))), None, true, false ; "important with rootless command")]
    #[test_case("text-blue-500/40", Some(SubjectValue::Value(Value("blue-500"))), Some(Value("40")), false, false ; "handles transparent")]
    #[test_case("text-white/40!", Some(SubjectValue::Value(Value("white"))), Some(Value("40")), true, false ; "handles transparent and important")]
    #[test_case("-m-4", Some(SubjectValue::Value(Value("4"))), None, false, true ; "handles negative")]
    fn expression(
        s: &str,
        value_exp: Option<SubjectValue>,
        transparency: Option<Value>,
        important: bool,
        negative: bool,
    ) {
        let (rest, d) = Expression::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();

        if let Subject::Literal(Literal { value, .. }) = d.subject {
            assert_eq!(value, value_exp);
        }

        assert_eq!(d.important, important);
        assert_eq!(d.alpha, transparency);
        assert_eq!(d.negative, negative);
        assert_matches!(*rest, "");
    }

    #[test_case("relative", Plugin::Position(Position::Relative), None ; "when a subject has no value")]
    #[test_case("pl-3.5", Plugin::Pl, Some(SubjectValue::Value(Value("3.5"))) ; "when a subject has a dot in it")]
    #[test_case("text-red-500", Plugin::Text(None), Some(SubjectValue::Value(Value("red-500"))) ; "when a subject has a dash")]
    #[test_case("border-b-4", Plugin::Border(Some(Border::B)), Some(SubjectValue::Value(Value("4"))) ; "dash in plugin")]
    #[test_case("border-4", Plugin::Border(None), Some(SubjectValue::Value(Value("4"))) ; "empty plugin subcommand")]
    #[test_case("max-w-4", Plugin::Max(Max::W), Some(SubjectValue::Value(Value("4"))) ; "rootless subcommand")]
    #[test_case("w-3/4", Plugin::W, Some(SubjectValue::Value(Value("3/4"))) ; "when a subject has a forward slash")]
    #[test_case("border-[10px]", Plugin::Border(None), Some(SubjectValue::Css(Css("10px"))) ; "arbitrary css")]
    #[test_case("border-[repeat(6,1fr)]", Plugin::Border(None), Some(SubjectValue::Css(Css("repeat(6,1fr)"))) ; "when braces are in arbitrary css")]
    #[test_case("border-[min-content min-content]", Plugin::Border(None), Some(SubjectValue::Css(Css("min-content min-content"))) ; "when spaces are in arbitrary css")]
    #[test_case("line-through", Plugin::TextDecoration(TextDecoration::LineThrough), None ; "when we have a transparent plugin")]
    #[test_case("table-cell", Plugin::Display(Display::TableCell), None ; "do not eagerly parse")]
    #[test_case("grid-flow-col", Plugin::Grid(Some(Grid::FlowCol)), None; "handles multiple words")]
    fn plugin(s: &str, p: Plugin, v: Option<SubjectValue>) {
        let (rest, s) = Subject::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();
        let lit = match s {
            Subject::Literal(l) => l,
            _ => panic!("should be a group"),
        };
        assert_eq!(lit.cmd, p, "correct plugin");
        assert_eq!(lit.value, v, "correct value");
        assert_matches!(*rest, "");
    }

    #[test_case("text-lg p-4" ; "basic")]
    #[test_case("2xl:text-right" ; "2xl modifier")]
    #[test_case("border-b-4 p-4" ; "with subcommand")]
    #[test_case("   p-4    border-4" ; "when a statement has irregular gaps")]
    #[test_case("dash-modifier:p-4" ; "when a modifier has a dash in it")]
    #[test_case("mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:max-w-7xl lg:px-8" ; "random prefixes")]
    #[test_case("relative rounded-2xl px-6 py-10 bg-primary-500 overflow-hidden shadow-xl sm:px-12 sm:py-20"; "example")]
    #[test_case("text-white/40 bg-white/50" ; "chained transparency")]
    #[test_case(r#"
p-4
    "# ; "newline")]
    fn directive_tests(s: &str) {
        let (rest, _d, errors) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
        assert_matches!(*rest, "");
        assert_eq!(errors, vec![]);
    }

    #[test_case(&["bg-white", "text-black"] ; "basic case")]
    #[test_case(&["prose", "w-full"] ; "prose")]
    #[test_case(&["text-sm", "font-bold", "text-black", "p-4", "m-8"] ; "more complicated")]
    fn directive_stable(s: &[&str]) {
        let mut config = TailwindConfig::default();
        config.theme.colors.insert("white", "#fff");
        config.theme.colors.insert("black", "#000");
        config.theme.width.insert("full", "100%");
        config.theme.margin.insert("auto", "auto");
        config.theme.font_weight.insert("bold", "bold");
        config.theme.padding.insert("4", "1rem");
        config.theme.margin.insert("8", "2rem");
        config
            .theme
            .font_size
            .insert("sm", ("1em", LineHeightOpt::Str("0.875rem")));

        let inputs = s
            .iter()
            .permutations(s.len())
            .map(|v| v.iter().copied().join(" "))
            .collect::<Vec<_>>();

        let lits = inputs
            .iter()
            .map(|s| {
                let (_, d, _e) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
                let (lit, _) = d.to_literal(&config);
                (s, sort_recursive(lit))
            })
            .collect::<Vec<_>>();

        for items in lits.windows(2) {
            let (s1, item1) = &items[0];
            let (s2, item2) = &items[1];

            assert_eq!(item1, item2, "\n\n`{}` != `{}`", s1, s2);
        }
    }

    #[test_case("-mod:sub" ; "when the minus is in the wrong place")]
    #[test_case("()" ; "rejects empty group")]
    fn parse_failure_tests(s: &str) {
        let (rest, _d, errs) = Directive::parse(LocatedSpan::new_extra(s, DUMMY_SP));
        assert_matches!(*rest, "");
        assert_eq!(errs.len(), 1);
    }

    #[test_case("40" ; "a number")]
    #[test_case("blue-500" ; "a color")]
    #[test_case("[10px]" ; "csss")]
    fn subject_value(s: &str) {
        let (rest, _s) = SubjectValue::parse(LocatedSpan::new_extra(s, DUMMY_SP)).unwrap();
        assert_matches!(*rest, "");
    }
}