glass_easel_stylesheet_compiler/
lib.rs

1use std::ops::Range;
2
3use cssparser::{CowRcStr, ParseError, ParserInput, Token};
4
5pub mod error;
6pub mod js_bindings;
7pub mod output;
8mod step;
9
10use output::StyleSheetOutput;
11use step::{StepParser, StepToken};
12
13#[derive(Debug, Clone, PartialEq)]
14pub struct StyleSheetOptions {
15    pub class_prefix: Option<String>,
16    pub class_prefix_sign: Option<String>,
17    pub rpx_ratio: f32,
18    pub import_sign: Option<String>,
19    pub convert_host: bool,
20    pub host_is: Option<String>,
21}
22
23impl Default for StyleSheetOptions {
24    fn default() -> Self {
25        Self {
26            class_prefix: None,
27            class_prefix_sign: None,
28            rpx_ratio: 750.,
29            import_sign: None,
30            convert_host: false,
31            host_is: None,
32        }
33    }
34}
35
36pub struct StyleSheetTransformer {
37    options: StyleSheetOptions,
38    path: String,
39    normal_output: StyleSheetOutput,
40    low_priority_output: StyleSheetOutput,
41    using_low_priority: bool,
42    warnings: Vec<error::ParseError>,
43    cur_at_rule_stacks: Vec<String>,
44}
45
46impl StyleSheetTransformer {
47    pub fn from_css(path: &str, css: &str, options: StyleSheetOptions) -> Self {
48        let parser_input = &mut ParserInput::new(css);
49        let parser = &mut cssparser::Parser::new(parser_input);
50        let mut input = StepParser::wrap(parser);
51
52        // construct this
53        let mut this = Self {
54            options,
55            path: path.to_string(),
56            normal_output: StyleSheetOutput::new(path, css),
57            low_priority_output: StyleSheetOutput::new(path, css),
58            using_low_priority: false,
59            warnings: vec![],
60            cur_at_rule_stacks: vec![],
61        };
62
63        {
64            parse_rules(&mut input, &mut this);
65        }
66        this
67    }
68
69    fn add_warning(&mut self, kind: error::ParseErrorKind, location: Range<error::Position>) {
70        self.warnings.push(error::ParseError {
71            path: self.path.clone(),
72            kind,
73            location,
74        });
75    }
76
77    pub fn warnings(&self) -> impl Iterator<Item = &error::ParseError> {
78        self.warnings.iter()
79    }
80
81    pub fn take_warnings(&mut self) -> Vec<error::ParseError> {
82        std::mem::replace(&mut self.warnings, vec![])
83    }
84
85    pub fn output(self) -> StyleSheetOutput {
86        self.normal_output
87    }
88
89    pub fn output_and_low_priority_output(self) -> (StyleSheetOutput, StyleSheetOutput) {
90        (self.normal_output, self.low_priority_output)
91    }
92
93    fn current_output(&self) -> &StyleSheetOutput {
94        if self.using_low_priority {
95            &self.low_priority_output
96        } else {
97            &self.normal_output
98        }
99    }
100
101    fn current_output_mut(&mut self) -> &mut StyleSheetOutput {
102        if self.using_low_priority {
103            &mut self.low_priority_output
104        } else {
105            &mut self.normal_output
106        }
107    }
108
109    fn append_nested_block(
110        &mut self,
111        token: StepToken,
112        _input: &mut StepParser,
113    ) -> StepToken<'static> {
114        let close = match &*token {
115            Token::CurlyBracketBlock => Token::CloseCurlyBracket,
116            Token::SquareBracketBlock => Token::CloseSquareBracket,
117            Token::ParenthesisBlock => Token::CloseParenthesis,
118            Token::Function(_) => Token::CloseParenthesis,
119            _ => unreachable!(),
120        };
121        let position = token.position;
122        self.current_output_mut().append_token(token, None);
123        StepToken::wrap(close, position)
124    }
125
126    fn append_nested_block_close(&mut self, close: StepToken<'static>, _input: &mut StepParser) {
127        self.current_output_mut().append_token(close, None);
128    }
129
130    fn write_in_low_priority<R>(
131        &mut self,
132        input: &mut StepParser,
133        f: impl FnOnce(&mut Self, &mut StepParser) -> R,
134    ) -> R {
135        self.using_low_priority = true;
136        for item in self.cur_at_rule_stacks.iter() {
137            self.low_priority_output.append_raw(&item);
138            self.low_priority_output.append_raw("{");
139        }
140        let r = f(self, input);
141        for _ in self.cur_at_rule_stacks.iter() {
142            self.low_priority_output.append_raw("}");
143        }
144        self.using_low_priority = false;
145        r
146    }
147
148    fn append_token(&mut self, token: StepToken, _input: &mut StepParser, src: Option<Token>) {
149        self.current_output_mut().append_token(token, src)
150    }
151
152    fn append_token_space_preserved(
153        &mut self,
154        token: StepToken,
155        _input: &mut StepParser,
156        src: Option<Token>,
157    ) {
158        self.current_output_mut()
159            .append_token_space_preserved(token, src)
160    }
161
162    fn cur_output_utf8_len(&self) -> usize {
163        self.current_output().cur_utf8_len()
164    }
165
166    fn get_output_segment(&self, range: std::ops::Range<usize>) -> &str {
167        self.current_output().get_output_segment(range)
168    }
169
170    fn wrap_at_rule_output<'a, R>(
171        &mut self,
172        input: &mut StepParser,
173        at_rule_str: String,
174        f: impl FnOnce(&mut Self, &mut StepParser) -> R,
175    ) -> R {
176        self.cur_at_rule_stacks.push(at_rule_str);
177        let r = f(self, input);
178        self.cur_at_rule_stacks.pop();
179        r
180    }
181}
182
183fn write_maybe_class_name(
184    input: &mut StepParser,
185    ss: &mut StyleSheetTransformer,
186    next: &StepToken,
187    src: &CowRcStr,
188    in_class: bool,
189) {
190    if in_class {
191        if let Some(content) = ss.options.class_prefix_sign.clone() {
192            let st = StepToken::wrap(Token::Comment(&content), next.position);
193            ss.append_token(st, input, None);
194        }
195    }
196    if in_class && ss.options.class_prefix.is_some() {
197        let s = format!("{}--{}", ss.options.class_prefix.as_ref().unwrap(), src);
198        let st = StepToken::wrap(Token::Ident(s.as_str().into()), next.position);
199        ss.append_token_space_preserved(st, input, Some(Token::Ident(src.clone())));
200    } else {
201        ss.append_token_space_preserved(next.clone(), input, None);
202    }
203}
204
205fn write_maybe_rpx_dimension(
206    input: &mut StepParser,
207    ss: &mut StyleSheetTransformer,
208    next: &StepToken,
209    has_sign: bool,
210    value: f32,
211    int_value: Option<i32>,
212    unit: &CowRcStr,
213) {
214    let unit_str: &str = &unit;
215    if unit_str == "rpx" {
216        let new_value = value * 100. / ss.options.rpx_ratio;
217        let new_int_value = if (new_value.round() - new_value).abs() <= f32::EPSILON {
218            Some(new_value.round() as i32)
219        } else {
220            None
221        };
222        let t = Token::Dimension {
223            has_sign,
224            value: new_value,
225            int_value: new_int_value,
226            unit: "vw".into(),
227        };
228        let st = StepToken::wrap(t, next.position);
229        ss.append_token(
230            st,
231            input,
232            Some(Token::Dimension {
233                has_sign,
234                value,
235                unit: unit.clone(),
236                int_value,
237            }),
238        );
239    } else {
240        let token = Token::Dimension {
241            has_sign,
242            value,
243            unit: unit.clone(),
244            int_value,
245        };
246        let st = StepToken::wrap(token, next.position);
247        ss.append_token(st, input, None);
248    }
249}
250
251fn parse_rules(input: &mut StepParser, ss: &mut StyleSheetTransformer) {
252    let mut at_file_start = true;
253    while !input.is_exhausted() {
254        if !parse_at_rule(input, ss, at_file_start) {
255            parse_qualified_rule(input, ss);
256        }
257        at_file_start = false;
258    }
259}
260
261fn parse_at_rule(
262    input: &mut StepParser,
263    ss: &mut StyleSheetTransformer,
264    at_file_start: bool,
265) -> bool {
266    let Ok(peek) = input.peek() else { return false };
267    if let Token::AtKeyword(x) = &*peek {
268        input.next().ok();
269        let at_keyword: &str = &x;
270        if at_keyword == "import" && ss.options.import_sign.is_some() {
271            // process at-import if needed
272            let import_sign = ss.options.import_sign.clone().unwrap();
273            let start_pos = input.position();
274            if !at_file_start {
275                ss.add_warning(
276                    error::ParseErrorKind::IllegalImportPosition,
277                    start_pos..start_pos,
278                );
279            }
280            let r = input.try_parse::<_, _, ParseError<()>>(|input| {
281                let rel_path = input.expect_string_cloned()?;
282                let mut close_stack = vec![];
283                let mut has_media = false;
284                while let Ok(peek) = input.peek() {
285                    match &*peek {
286                        Token::Function(x) => {
287                            let xs: &str = &x;
288                            if !matches!(xs, "layer" | "supports") {
289                                ss.add_warning(
290                                    error::ParseErrorKind::UnexpectedCharacter,
291                                    peek.position..peek.position,
292                                );
293                                break;
294                            }
295                            input.next().ok();
296                            let st = StepToken::wrap(Token::AtKeyword(x.clone()), peek.position);
297                            ss.append_token(st, input, Some(peek.token.clone()));
298                            match xs {
299                                "layer" => {
300                                    convert_class_names_and_rpx_in_block(input, ss);
301                                }
302                                "supports" => {
303                                    let st =
304                                        StepToken::wrap(Token::ParenthesisBlock, peek.position);
305                                    let close = ss.append_nested_block(st, input);
306                                    convert_class_names_and_rpx_in_block(input, ss);
307                                    ss.append_nested_block_close(close, input);
308                                }
309                                _ => unreachable!(),
310                            }
311                            let st = StepToken::wrap(Token::CurlyBracketBlock, peek.position);
312                            let close = ss.append_nested_block(st, input);
313                            close_stack.push(close);
314                        }
315                        Token::Ident(_) | Token::ParenthesisBlock => {
316                            has_media = true;
317                            break;
318                        }
319                        Token::Semicolon => {
320                            input.next().ok();
321                            break;
322                        }
323                        _ => {
324                            ss.add_warning(
325                                error::ParseErrorKind::UnexpectedCharacter,
326                                peek.position..peek.position,
327                            );
328                            return Err(input.new_error_for_next_token());
329                        }
330                    }
331                }
332                let pos = input.position();
333                if has_media {
334                    let st = StepToken::wrap(Token::AtKeyword("media".into()), start_pos);
335                    ss.append_token(st, input, None);
336                    while let Ok(next) = input.next() {
337                        match &*next {
338                            Token::CurlyBracketBlock => {
339                                ss.add_warning(
340                                    error::ParseErrorKind::UnexpectedCharacter,
341                                    pos..pos,
342                                );
343                                return Err(input.new_error_for_next_token());
344                            }
345                            Token::SquareBracketBlock
346                            | Token::ParenthesisBlock
347                            | Token::Function(_) => {
348                                let close = ss.append_nested_block(next, input);
349                                convert_class_names_and_rpx_in_block(input, ss);
350                                ss.append_nested_block_close(close, input);
351                            }
352                            Token::Semicolon => {
353                                break;
354                            }
355                            _ => {
356                                ss.append_token(next, input, None);
357                            }
358                        }
359                    }
360                    let st = StepToken::wrap(Token::CurlyBracketBlock, start_pos);
361                    let close = ss.append_nested_block(st, input);
362                    close_stack.push(close);
363                }
364                let comment = format!("{} {}", import_sign, urlencoding::encode(&rel_path));
365                let st = StepToken::wrap(Token::Comment(comment.as_str()), start_pos);
366                ss.append_token(st, input, None);
367                while let Some(close) = close_stack.pop() {
368                    ss.append_nested_block_close(close, input);
369                }
370                Ok(())
371            });
372            if r.is_err() {
373                while let Ok(x) = input.next() {
374                    match &*x {
375                        Token::CurlyBracketBlock | Token::Semicolon => break,
376                        _ => {}
377                    }
378                }
379            }
380        } else {
381            // process other at-rules
382            let st = StepToken::wrap(Token::AtKeyword(x.clone()), peek.position);
383            let output_index = ss.cur_output_utf8_len();
384            ss.append_token(st, input, None);
385            let x: &str = &x;
386            let contain_rule_list = matches!(x, "media" | "supports" | "document");
387            loop {
388                let r = input.try_parse::<_, _, ParseError<()>>(|input| {
389                    let next = input.next()?;
390                    match next.token.clone() {
391                        Token::CurlyBracketBlock => {
392                            let at_rule_str = ss
393                                .get_output_segment(output_index..ss.cur_output_utf8_len())
394                                .to_string();
395                            ss.wrap_at_rule_output(input, at_rule_str, |ss, input| {
396                                let close = ss.append_nested_block(next, input);
397                                if contain_rule_list {
398                                    input
399                                        .parse_nested_block::<_, (), ()>(|nested_input| {
400                                            let input = &mut StepParser::wrap(nested_input);
401                                            parse_rules(input, ss);
402                                            Ok(())
403                                        })
404                                        .ok();
405                                } else {
406                                    convert_rpx_in_block(input, ss, None);
407                                }
408                                ss.append_nested_block_close(close, input);
409                            });
410                            return Ok(false);
411                        }
412                        Token::SquareBracketBlock
413                        | Token::ParenthesisBlock
414                        | Token::Function(_) => {
415                            let close = ss.append_nested_block(next, input);
416                            convert_class_names_and_rpx_in_block(input, ss);
417                            ss.append_nested_block_close(close, input);
418                        }
419                        Token::Semicolon => {
420                            ss.append_token(next, input, None);
421                            return Ok(false);
422                        }
423                        _ => {
424                            ss.append_token(next, input, None);
425                        }
426                    }
427                    Ok(true)
428                });
429                match r {
430                    Ok(cont) => {
431                        if !cont {
432                            break;
433                        }
434                    }
435                    Err(_) => break,
436                }
437            }
438        }
439        true
440    } else {
441        false
442    }
443}
444
445fn parse_qualified_rule(input: &mut StepParser, ss: &mut StyleSheetTransformer) {
446    let mut in_class = false;
447    let mut has_whitespace = false;
448    input.skip_whitespace();
449    if ss.options.convert_host {
450        let r = input.try_parse::<_, _, ParseError<()>>(|input| {
451            input.expect_colon()?;
452            let Ok(next) = input.next() else {
453                return Ok(());
454            };
455            let mut invalid = match &*next {
456                Token::Ident(x) if x.as_bytes() == b"host" => None,
457                Token::Function(x) if x.as_bytes() == b"host" => Some(input.position()),
458                _ => return Err(input.new_custom_error(())),
459            };
460            let next = loop {
461                let Ok(next) = input.next() else {
462                    return Ok(());
463                };
464                if *next != Token::CurlyBracketBlock {
465                    if invalid.is_none() {
466                        invalid = Some(input.position());
467                    }
468                } else {
469                    break next;
470                }
471            };
472            if let Some(pos) = invalid {
473                ss.add_warning(error::ParseErrorKind::HostSelectorCombination, pos..pos);
474            } else {
475                ss.write_in_low_priority(input, |ss, input| {
476                    let write_attr_selector =
477                        |ss: &mut StyleSheetTransformer,
478                         input: &mut StepParser,
479                         name: &str,
480                         value: &str| {
481                            ss.append_token(
482                                StepToken::wrap_at(Token::SquareBracketBlock, &next),
483                                input,
484                                None,
485                            );
486                            ss.append_token(
487                                StepToken::wrap_at(Token::Ident(name.into()), &next),
488                                input,
489                                None,
490                            );
491                            ss.append_token(
492                                StepToken::wrap_at(Token::Delim('='), &next),
493                                input,
494                                None,
495                            );
496                            let quoted_value = Token::QuotedString(value.into());
497                            ss.append_token(StepToken::wrap_at(quoted_value, &next), input, None);
498                            ss.append_token(
499                                StepToken::wrap_at(Token::CloseSquareBracket, &next),
500                                input,
501                                None,
502                            );
503                        };
504                    let p = ss.options.class_prefix.clone().unwrap_or_default();
505                    write_attr_selector(ss, input, "wx-host", &p);
506                    if let Some(host_is) = ss.options.host_is.clone() {
507                        ss.append_token(StepToken::wrap_at(Token::Comma, &next), input, None);
508                        write_attr_selector(ss, input, "is", &host_is);
509                    }
510                    let close = ss.append_nested_block(next, input);
511                    convert_rpx_in_block(input, ss, None);
512                    ss.append_nested_block_close(close, input);
513                });
514            }
515            Ok(())
516        });
517        if r.is_ok() {
518            return;
519        }
520    }
521    loop {
522        let r = input.try_parse::<_, _, ParseError<()>>(|input| {
523            let next = input.next_including_whitespace()?;
524            match &*next {
525                Token::CurlyBracketBlock | Token::WhiteSpace(_) => {}
526                _ => {
527                    if has_whitespace {
528                        let st = StepToken::wrap(Token::WhiteSpace(" "), next.position);
529                        ss.append_token_space_preserved(st, input, None);
530                    }
531                }
532            }
533            has_whitespace = false;
534            match &*next {
535                Token::CurlyBracketBlock => {
536                    let close = ss.append_nested_block(next, input);
537                    convert_rpx_in_block(input, ss, None);
538                    ss.append_nested_block_close(close, input);
539                    return Ok(false);
540                }
541                Token::SquareBracketBlock | Token::ParenthesisBlock | Token::Function(_) => {
542                    let close = ss.append_nested_block(next, input);
543                    convert_class_names_and_rpx_in_block(input, ss);
544                    ss.append_nested_block_close(close, input);
545                    in_class = false;
546                }
547                Token::Delim('.') => {
548                    ss.append_token_space_preserved(next, input, None);
549                    in_class = true;
550                }
551                Token::Ident(src) => {
552                    write_maybe_class_name(input, ss, &next, src, in_class);
553                    in_class = false;
554                }
555                Token::WhiteSpace(_) => {
556                    has_whitespace = true;
557                    in_class = false;
558                }
559                _ => {
560                    ss.append_token_space_preserved(next.clone(), input, None);
561                    in_class = false;
562                }
563            }
564            Ok(true)
565        });
566        match r {
567            Ok(cont) => {
568                if !cont {
569                    break;
570                }
571            }
572            Err(_) => break,
573        }
574    }
575}
576
577fn convert_class_names_and_rpx_in_block(input: &mut StepParser, ss: &mut StyleSheetTransformer) {
578    input
579        .parse_nested_block::<_, (), ()>(|nested_input| {
580            let input = &mut StepParser::wrap(nested_input);
581            let mut in_class = false;
582            let mut has_whitespace = false;
583            input.skip_whitespace();
584            loop {
585                let next = input.next_including_whitespace()?;
586                match &*next {
587                    Token::CurlyBracketBlock | Token::WhiteSpace(_) => {}
588                    _ => {
589                        if has_whitespace {
590                            let st = StepToken::wrap(Token::WhiteSpace(" "), next.position);
591                            ss.append_token_space_preserved(st, input, None);
592                        }
593                    }
594                }
595                has_whitespace = false;
596                match &*next {
597                    Token::CurlyBracketBlock
598                    | Token::SquareBracketBlock
599                    | Token::ParenthesisBlock => {
600                        let close = ss.append_nested_block(next, input);
601                        convert_class_names_and_rpx_in_block(input, ss);
602                        ss.append_nested_block_close(close, input);
603                        in_class = false;
604                    }
605                    Token::Function(func) => {
606                        let func: &str = func;
607                        let config = if func == "calc" {
608                            Some(ConvertOptions { in_calc: true })
609                        } else {
610                            None
611                        };
612                        let close = ss.append_nested_block(next.clone(), input);
613                        convert_rpx_in_block(input, ss, config);
614                        ss.append_nested_block_close(close, input);
615                        in_class = false;
616                    }
617                    Token::Delim('.') => {
618                        ss.append_token(next, input, None);
619                        in_class = true;
620                    }
621                    Token::Ident(src) => {
622                        write_maybe_class_name(input, ss, &next, src, in_class);
623                        in_class = false;
624                    }
625                    Token::Dimension {
626                        has_sign,
627                        value,
628                        unit,
629                        int_value,
630                    } => {
631                        write_maybe_rpx_dimension(
632                            input, ss, &next, *has_sign, *value, *int_value, unit,
633                        );
634                        in_class = false;
635                    }
636                    Token::WhiteSpace(_) => {
637                        has_whitespace = true;
638                        in_class = false;
639                    }
640                    _ => {
641                        ss.append_token(next, input, None);
642                        in_class = false;
643                    }
644                }
645            }
646        })
647        .ok();
648}
649
650struct ConvertOptions {
651    in_calc: bool,
652}
653
654fn convert_rpx_in_block(
655    input: &mut StepParser,
656    ss: &mut StyleSheetTransformer,
657    convert_options: Option<ConvertOptions>,
658) {
659    let mut skip_whitespace = true;
660    let mut in_calc = false;
661    if let Some(options) = convert_options {
662        if options.in_calc {
663            skip_whitespace = false;
664            in_calc = true;
665        }
666    }
667    input
668        .parse_nested_block::<_, (), ()>(|nested_input| {
669            let input = &mut StepParser::wrap(nested_input);
670            let mut prev_token: Option<StepToken> = None;
671            loop {
672                let next = if skip_whitespace {
673                    input.next()?
674                } else {
675                    input.next_including_whitespace()?
676                };
677                match &*next {
678                    Token::CurlyBracketBlock
679                    | Token::SquareBracketBlock
680                    | Token::ParenthesisBlock => {
681                        let close = ss.append_nested_block(next.clone(), input);
682                        convert_rpx_in_block(input, ss, None);
683                        ss.append_nested_block_close(close, input);
684                    }
685                    Token::Function(func) => {
686                        let func: &str = func;
687                        let config = if func == "calc" {
688                            Some(ConvertOptions { in_calc: true })
689                        } else {
690                            None
691                        };
692                        let close = ss.append_nested_block(next.clone(), input);
693                        convert_rpx_in_block(input, ss, config);
694                        ss.append_nested_block_close(close, input);
695                    }
696                    Token::Dimension {
697                        has_sign,
698                        value,
699                        unit,
700                        int_value,
701                    } => {
702                        write_maybe_rpx_dimension(
703                            input, ss, &next, *has_sign, *value, *int_value, unit,
704                        );
705                    }
706                    Token::WhiteSpace(_) => {
707                        let mut skip = true;
708                        if in_calc {
709                            // In calc(), the + and - operators must be surrounded by whitespace.
710                            // match next token
711                            let _ = input.try_parse::<_, (), ()>(|input| {
712                                let next_token =
713                                    input.next_including_whitespace().map_err(|_| ())?;
714                                match &*next_token {
715                                    Token::Delim(c) if *c == '+' || *c == '-' => {
716                                        skip = false;
717                                    }
718                                    _ => {}
719                                }
720                                Err(())
721                            });
722                            // match prev token
723                            if let Some(prev_token) = prev_token {
724                                match &*prev_token {
725                                    Token::Delim(c) if *c == '+' || *c == '-' => {
726                                        skip = false;
727                                    }
728                                    _ => {}
729                                }
730                            }
731                        }
732                        if !skip {
733                            let st = StepToken::wrap(Token::WhiteSpace(" "), next.position);
734                            ss.append_token(st, input, None);
735                        }
736                    }
737                    _ => {
738                        ss.append_token(next.clone(), input, None);
739                    }
740                }
741                prev_token = Some(next);
742            }
743        })
744        .ok();
745}
746
747#[cfg(test)]
748mod test {
749    use sourcemap::SourceMap;
750
751    use super::*;
752
753    #[test]
754    fn remove_comments() {
755        let trans = StyleSheetTransformer::from_css(
756            "",
757            r#" /* test */ .a { } "#,
758            StyleSheetOptions {
759                ..Default::default()
760            },
761        );
762        let mut s = Vec::new();
763        let output = trans.output();
764        output.write(&mut s).unwrap();
765        let mut sm = Vec::new();
766        output.write_source_map(&mut sm).unwrap();
767        assert_eq!(std::str::from_utf8(&s).unwrap(), ".a{}");
768    }
769
770    #[test]
771    fn add_class_prefix() {
772        let trans = StyleSheetTransformer::from_css(
773            "",
774            r#"
775                #a.b   [g] .c.d g:e(.f) {
776                    key: .v f(.c) d.e;
777                }
778            "#,
779            StyleSheetOptions {
780                class_prefix: Some("p".into()),
781                ..Default::default()
782            },
783        );
784        let mut s = Vec::new();
785        let output = trans.output();
786        output.write(&mut s).unwrap();
787        let mut sm = Vec::new();
788        output.write_source_map(&mut sm).unwrap();
789        assert_eq!(
790            std::str::from_utf8(&s).unwrap(),
791            "#a.p--b [g] .p--c.p--d g:e(.p--f){key:.v f(.c)d.e;}"
792        );
793    }
794
795    #[test]
796    fn allow_class_prefix_sign() {
797        let trans = StyleSheetTransformer::from_css(
798            "",
799            r#"
800                .a g:e(.f) {}
801            "#,
802            StyleSheetOptions {
803                class_prefix: None,
804                class_prefix_sign: Some("TEST".into()),
805                ..Default::default()
806            },
807        );
808        let mut s = Vec::new();
809        let output = trans.output();
810        output.write(&mut s).unwrap();
811        let mut sm = Vec::new();
812        output.write_source_map(&mut sm).unwrap();
813        assert_eq!(
814            std::str::from_utf8(&s).unwrap(),
815            "./*TEST*/a g:e(./*TEST*/f){}"
816        );
817    }
818
819    #[test]
820    fn allow_class_prefix_sign_before_prefix() {
821        let trans = StyleSheetTransformer::from_css(
822            "",
823            r#"
824                .a {}
825            "#,
826            StyleSheetOptions {
827                class_prefix: Some("p".into()),
828                class_prefix_sign: Some("TEST".into()),
829                ..Default::default()
830            },
831        );
832        let mut s = Vec::new();
833        let output = trans.output();
834        output.write(&mut s).unwrap();
835        let mut sm = Vec::new();
836        output.write_source_map(&mut sm).unwrap();
837        assert_eq!(std::str::from_utf8(&s).unwrap(), "./*TEST*/p--a{}");
838    }
839
840    #[test]
841    fn transform_rpx() {
842        let trans = StyleSheetTransformer::from_css(
843            "",
844            r#"
845                #a.b[1rpx]  (7.5rpx){
846                    key: 7.5rpx f(7.5rpx);
847                }
848            "#,
849            StyleSheetOptions {
850                class_prefix: Some("p".into()),
851                ..Default::default()
852            },
853        );
854        let mut s = Vec::new();
855        let output = trans.output();
856        output.write(&mut s).unwrap();
857        assert_eq!(
858            std::str::from_utf8(&s).unwrap(),
859            "#a.p--b[0.133333vw] (1vw){key:1vw f(1vw);}"
860        );
861    }
862
863    #[test]
864    fn transform_rpx_in_simple_at_rules() {
865        let trans = StyleSheetTransformer::from_css(
866            "",
867            r#"
868                @a 75rpx;
869                .b {}
870                @c (75rpx .d) { 75rpx .e }
871                .f {}
872            "#,
873            StyleSheetOptions {
874                class_prefix: Some("p".into()),
875                ..Default::default()
876            },
877        );
878        let mut s = Vec::new();
879        let output = trans.output();
880        output.write(&mut s).unwrap();
881        assert_eq!(
882            std::str::from_utf8(&s).unwrap(),
883            "@a 75rpx;.p--b{}@c(10vw .p--d){10vw.e}.p--f{}"
884        );
885    }
886
887    #[test]
888    fn rules_in_at_media() {
889        let trans = StyleSheetTransformer::from_css(
890            "",
891            r#"
892                @media (width: 1rpx) {
893                    .b [2rpx] {
894                        key: 3rpx .a;
895                    }
896                }
897            "#,
898            StyleSheetOptions {
899                class_prefix: Some("p".into()),
900                rpx_ratio: 10.,
901                ..Default::default()
902            },
903        );
904        let mut s = Vec::new();
905        let output = trans.output();
906        output.write(&mut s).unwrap();
907        assert_eq!(
908            std::str::from_utf8(&s).unwrap(),
909            "@media(width: 10vw){.p--b [20vw]{key:30vw.a;}}"
910        );
911    }
912
913    #[test]
914    fn host_select() {
915        let trans = StyleSheetTransformer::from_css(
916            "",
917            r#"
918                :host {
919                    color: red;
920                }
921            "#,
922            StyleSheetOptions {
923                class_prefix: Some("ABC".into()),
924                convert_host: true,
925                ..Default::default()
926            },
927        );
928        let (output, lp) = trans.output_and_low_priority_output();
929        let mut s = Vec::new();
930        output.write(&mut s).unwrap();
931        assert!(std::str::from_utf8(&s).unwrap().is_empty());
932        let mut s = Vec::new();
933        lp.write(&mut s).unwrap();
934        assert_eq!(
935            std::str::from_utf8(&s).unwrap(),
936            r#"[wx-host="ABC"]{color:red;}"#
937        );
938    }
939
940    #[test]
941    fn illegal_host_combination() {
942        let trans = StyleSheetTransformer::from_css(
943            "",
944            r#"
945                :host(.a) {
946                    color: red;
947                }
948                :host .a {
949                    color: red;
950                }
951                .a { color: green }
952            "#,
953            StyleSheetOptions {
954                convert_host: true,
955                ..Default::default()
956            },
957        );
958        assert_eq!(
959            trans.warnings().map(|x| x.kind.clone()).collect::<Vec<_>>(),
960            [
961                error::ParseErrorKind::HostSelectorCombination,
962                error::ParseErrorKind::HostSelectorCombination
963            ],
964        );
965        let (output, lp) = trans.output_and_low_priority_output();
966        let mut s = Vec::new();
967        output.write(&mut s).unwrap();
968        assert_eq!(std::str::from_utf8(&s).unwrap(), r#".a{color:green}"#);
969        let mut s = Vec::new();
970        lp.write(&mut s).unwrap();
971        assert_eq!(std::str::from_utf8(&s).unwrap(), r#""#);
972    }
973
974    #[test]
975    fn host_select_inside_at_rules() {
976        let trans = StyleSheetTransformer::from_css(
977            "",
978            r#"
979                @media (width: 1px) {
980                    @supports (color: red) {
981                        .a {
982                            color: red;
983                        }
984                        :host {
985                            color: pink;
986                        }
987                        .b {
988                            color: green;
989                        }
990                    }
991                }
992            "#,
993            StyleSheetOptions {
994                class_prefix: None,
995                convert_host: true,
996                host_is: Some("IS".into()),
997                ..Default::default()
998            },
999        );
1000        let (output, lp) = trans.output_and_low_priority_output();
1001        let mut s = Vec::new();
1002        output.write(&mut s).unwrap();
1003        assert_eq!(
1004            std::str::from_utf8(&s).unwrap(),
1005            r#"@media(width: 1px){@supports(color: red){.a{color:red;}.b{color:green;}}}"#
1006        );
1007        let mut s = Vec::new();
1008        lp.write(&mut s).unwrap();
1009        assert_eq!(
1010            std::str::from_utf8(&s).unwrap(),
1011            r#"@media(width: 1px){@supports(color: red){[wx-host=""],[is="IS"]{color:pink;}}}"#
1012        );
1013    }
1014
1015    #[test]
1016    fn import_sign_urlencoded() {
1017        let trans = StyleSheetTransformer::from_css(
1018            "",
1019            r#"
1020                @import './a\\b*?'
1021            "#,
1022            StyleSheetOptions {
1023                import_sign: Some("TEST".into()),
1024                ..Default::default()
1025            },
1026        );
1027        let mut s = Vec::new();
1028        let output = trans.output();
1029        output.write(&mut s).unwrap();
1030        assert_eq!(
1031            std::str::from_utf8(&s).unwrap(),
1032            r#"/*TEST .%2Fa%5Cb%2A%3F*/"#
1033        );
1034    }
1035
1036    #[test]
1037    fn import_sign_with_media_queries() {
1038        let trans = StyleSheetTransformer::from_css(
1039            "",
1040            r#"
1041                @import './a' (min-width: 10px);
1042                .a { }
1043            "#,
1044            StyleSheetOptions {
1045                import_sign: Some("TEST".into()),
1046                ..Default::default()
1047            },
1048        );
1049        let mut s = Vec::new();
1050        let output = trans.output();
1051        output.write(&mut s).unwrap();
1052        assert_eq!(
1053            std::str::from_utf8(&s).unwrap(),
1054            r#"@media(min-width: 10px){/*TEST .%2Fa*/}.a{}"#
1055        );
1056    }
1057
1058    #[test]
1059    fn import_sign_with_supports() {
1060        let trans = StyleSheetTransformer::from_css(
1061            "",
1062            r#"
1063                @import './a' layer(a) supports(color: red) print and (min-width: 10px);
1064            "#,
1065            StyleSheetOptions {
1066                import_sign: Some("TEST".into()),
1067                ..Default::default()
1068            },
1069        );
1070        let mut s = Vec::new();
1071        let output = trans.output();
1072        output.write(&mut s).unwrap();
1073        assert_eq!(
1074            std::str::from_utf8(&s).unwrap(),
1075            r#"@layer a{@supports(color: red){@media print and (min-width: 10px){/*TEST .%2Fa*/}}}"#
1076        );
1077    }
1078
1079    #[test]
1080    fn import_sign_not_at_top() {
1081        let trans = StyleSheetTransformer::from_css(
1082            "",
1083            r#"
1084                .a {}
1085                @import './a';
1086            "#,
1087            StyleSheetOptions {
1088                import_sign: Some("TEST".into()),
1089                ..Default::default()
1090            },
1091        );
1092        let mut s = Vec::new();
1093        assert_eq!(
1094            trans.warnings().map(|x| x.kind.clone()).collect::<Vec<_>>(),
1095            [error::ParseErrorKind::IllegalImportPosition],
1096        );
1097        let output = trans.output();
1098        output.write(&mut s).unwrap();
1099        assert_eq!(std::str::from_utf8(&s).unwrap(), r#".a{}/*TEST .%2Fa*/"#);
1100    }
1101
1102    #[test]
1103    fn minify_calc() {
1104        let trans = StyleSheetTransformer::from_css(
1105            "",
1106            r#"
1107                .a {
1108                    margin: 10px 20rpx 30px calc(10px + 20px / 2);
1109                    padding: calc(10rpx  *  2  +  30px);
1110                }
1111            "#,
1112            StyleSheetOptions {
1113                class_prefix: Some("p".into()),
1114                rpx_ratio: 10.,
1115                ..Default::default()
1116            },
1117        );
1118        let mut s = Vec::new();
1119        let output = trans.output();
1120        output.write(&mut s).unwrap();
1121        assert_eq!(
1122            std::str::from_utf8(&s).unwrap(),
1123            ".p--a{margin:10px 200vw 30px calc(10px + 20px/2);padding:calc(100vw*2 + 30px);}"
1124        );
1125    }
1126
1127    #[test]
1128    fn minify_calc_source_map() {
1129        let trans = StyleSheetTransformer::from_css(
1130            "",
1131            r#" .a { padding: calc(10rpx  *  2  +  30px); } "#,
1132            StyleSheetOptions {
1133                class_prefix: Some("p".into()),
1134                rpx_ratio: 10.,
1135                ..Default::default()
1136            },
1137        );
1138        let mut s = Vec::new();
1139        let output = trans.output();
1140        output.write(&mut s).unwrap();
1141        assert_eq!(
1142            std::str::from_utf8(&s).unwrap(),
1143            ".p--a{padding:calc(100vw*2 + 30px);}"
1144        );
1145        let mut sm = Vec::new();
1146        output.write_source_map(&mut sm).unwrap();
1147        let sm: &[u8] = &sm;
1148        let source_map = SourceMap::from_reader(sm).unwrap();
1149        {
1150            let token = source_map.lookup_token(0, 0).unwrap();
1151            assert_eq!(token.get_name(), None);
1152            assert_eq!(token.get_src_line(), 0);
1153            assert_eq!(token.get_src_col(), 1);
1154        }
1155        {
1156            let token = source_map.lookup_token(0, 1).unwrap();
1157            assert_eq!(token.get_src_line(), 0);
1158            assert_eq!(token.get_src_col(), 2);
1159        }
1160        {
1161            let token = source_map.lookup_token(0, 5).unwrap();
1162            assert_eq!(token.get_src_line(), 0);
1163            assert_eq!(token.get_src_col(), 4);
1164        }
1165        {
1166            let token = source_map.lookup_token(0, 6).unwrap();
1167            assert_eq!(token.get_src_line(), 0);
1168            assert_eq!(token.get_src_col(), 6);
1169        }
1170        {
1171            let token = source_map.lookup_token(0, 14).unwrap();
1172            assert_eq!(token.get_src_line(), 0);
1173            assert_eq!(token.get_src_col(), 15);
1174        }
1175        {
1176            let token = source_map.lookup_token(0, 19).unwrap();
1177            assert_eq!(token.get_src_line(), 0);
1178            assert_eq!(token.get_src_col(), 20);
1179        }
1180        {
1181            let token = source_map.lookup_token(0, 24).unwrap();
1182            assert_eq!(token.get_src_line(), 0);
1183            assert_eq!(token.get_src_col(), 27);
1184        }
1185        {
1186            let token = source_map.lookup_token(0, 27).unwrap();
1187            assert_eq!(token.get_src_line(), 0);
1188            assert_eq!(token.get_src_col(), 33);
1189        }
1190        {
1191            let token = source_map.lookup_token(0, 29).unwrap();
1192            assert_eq!(token.get_src_line(), 0);
1193            assert_eq!(token.get_src_col(), 36);
1194        }
1195    }
1196
1197    #[test]
1198    fn source_map() {
1199        let trans = StyleSheetTransformer::from_css(
1200            "",
1201            r#" .a { width: 1rpx } "#,
1202            StyleSheetOptions {
1203                class_prefix: Some("p".into()),
1204                rpx_ratio: 10.,
1205                ..Default::default()
1206            },
1207        );
1208        let mut s = Vec::new();
1209        let output = trans.output();
1210        output.write(&mut s).unwrap();
1211        assert_eq!(std::str::from_utf8(&s).unwrap(), ".p--a{width:10vw}");
1212        let mut sm = Vec::new();
1213        output.write_source_map(&mut sm).unwrap();
1214        let sm: &[u8] = &sm;
1215        let source_map = SourceMap::from_reader(sm).unwrap();
1216        {
1217            let token = source_map.lookup_token(0, 0).unwrap();
1218            assert_eq!(token.get_name(), None);
1219            assert_eq!(token.get_src_line(), 0);
1220            assert_eq!(token.get_src_col(), 1);
1221        }
1222        {
1223            let token = source_map.lookup_token(0, 1).unwrap();
1224            assert_eq!(token.get_src_line(), 0);
1225            assert_eq!(token.get_src_col(), 2);
1226        }
1227        {
1228            let token = source_map.lookup_token(0, 5).unwrap();
1229            assert_eq!(token.get_src_line(), 0);
1230            assert_eq!(token.get_src_col(), 4);
1231        }
1232        {
1233            let token = source_map.lookup_token(0, 6).unwrap();
1234            assert_eq!(token.get_src_line(), 0);
1235            assert_eq!(token.get_src_col(), 6);
1236        }
1237        {
1238            let token = source_map.lookup_token(0, 12).unwrap();
1239            assert_eq!(token.get_src_line(), 0);
1240            assert_eq!(token.get_src_col(), 13);
1241        }
1242    }
1243}