rcss_core/
rcss_at_rule.rs

1//! Implementation of @rcss(..) at rule
2//!
3//! The aims of this at-rule is to:
4//! - Merge mod and inline implementation of rcss macro.
5//! - Allow changing parsing behaviour.
6
7use std::{fmt::Debug, str::FromStr};
8
9use lightningcss::{
10    traits::AtRuleParser,
11    visitor::{Visit, VisitTypes, Visitor},
12};
13use proc_macro2::{TokenStream, TokenTree};
14use quote::{ToTokens, TokenStreamExt};
15use thiserror::Error;
16
17use syn::{ItemStruct, Path, Token};
18pub struct RcssAtRuleParser;
19
20#[derive(Clone)]
21pub enum RcssAtRuleConfig {
22    Struct(ItemStruct),
23    Extend(Path),
24}
25impl Debug for RcssAtRuleConfig {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            RcssAtRuleConfig::Struct(item_mod) => write!(f, "Mod{}", item_mod.to_token_stream()),
29            RcssAtRuleConfig::Extend(path) => write!(f, "Extend{}", path.to_token_stream()),
30        }
31    }
32}
33impl RcssAtRuleConfig {
34    pub fn from_token_stream(tokens: TokenStream) -> Result<Self, AtRuleError> {
35        let mut iter = tokens.clone().into_iter();
36
37        if matches!(iter.next(), Some(TokenTree::Ident(i)) if i.to_string() == "extend") {
38            let tokens = iter.collect();
39            let result = syn::parse2::<Path>(tokens)?;
40            Ok(RcssAtRuleConfig::Extend(result))
41        } else {
42            let mut tokens = tokens;
43            // append semicolon, to statisfy syn::parse2::<ItemStruct>
44            Token![;](proc_macro2::Span::call_site()).to_tokens(&mut tokens);
45
46            let mut result = syn::parse2::<ItemStruct>(tokens)?;
47            // TODO: Check instead of adding.
48            result.fields = syn::Fields::Unit;
49            result.semi_token = None;
50            Ok(RcssAtRuleConfig::Struct(result))
51        }
52    }
53}
54
55#[derive(Debug, Error)]
56pub enum AtRuleError {
57    #[error("Unexpected at-rule, expected only rcss extension")]
58    UnexpectedAtRule,
59    #[error("Rcss rule has no block")]
60    UnexpectedBlock,
61    #[error("Failed to parse rcss rule as syn expression")]
62    ErrorFromSyn(#[from] syn::Error),
63    #[error("Failed to parse rcss rule as rust code")]
64    TokenStreamError(#[from] proc_macro2::LexError),
65}
66
67impl<'i> AtRuleParser<'i> for RcssAtRuleParser {
68    type Prelude = RcssAtRuleConfig;
69    type AtRule = RcssAtRuleConfig;
70    type Error = AtRuleError;
71
72    fn parse_prelude<'t>(
73        &mut self,
74        name: cssparser::CowRcStr<'i>,
75        input: &mut cssparser::Parser<'i, 't>,
76        _options: &lightningcss::stylesheet::ParserOptions<'_, 'i>,
77    ) -> Result<Self::Prelude, cssparser::ParseError<'i, Self::Error>> {
78        if name != "rcss" {
79            return Err(input.new_custom_error(AtRuleError::UnexpectedAtRule));
80        }
81        input.expect_parenthesis_block()?;
82        let stream = input.parse_nested_block(|input| {
83            let start = input.state().position();
84            while let Ok(_v) = input.next() {
85                // skip tokens to parse them later with syn
86            }
87
88            Ok(input.slice_from(start))
89        })?;
90
91        let stream = stream.trim();
92
93        let tokens =
94            proc_macro2::TokenStream::from_str(stream).map_err(|e| input.new_custom_error(e))?;
95
96        RcssAtRuleConfig::from_token_stream(tokens).map_err(|e| input.new_custom_error(e))
97    }
98
99    fn parse_block<'t>(
100        &mut self,
101        _prelude: Self::Prelude,
102        _start: &cssparser::ParserState,
103        input: &mut cssparser::Parser<'i, 't>,
104        _options: &lightningcss::stylesheet::ParserOptions<'_, 'i>,
105        _is_nested: bool,
106    ) -> Result<Self::AtRule, cssparser::ParseError<'i, Self::Error>> {
107        Err(input.new_custom_error(AtRuleError::UnexpectedBlock))
108    }
109
110    fn rule_without_block(
111        &mut self,
112        prelude: Self::Prelude,
113        _start: &cssparser::ParserState,
114        _options: &lightningcss::stylesheet::ParserOptions<'_, 'i>,
115        _is_nested: bool,
116    ) -> Result<Self::AtRule, ()> {
117        Ok(prelude)
118    }
119}
120
121impl lightningcss::traits::ToCss for RcssAtRuleConfig {
122    fn to_css<W>(
123        &self,
124        dest: &mut lightningcss::printer::Printer<W>,
125    ) -> Result<(), lightningcss::error::PrinterError>
126    where
127        W: std::fmt::Write,
128    {
129        let args = match self {
130            RcssAtRuleConfig::Struct(item_mod) => {
131                // Don't use neither semicolon, nor block in ItemStruct
132                let mut tokens = TokenStream::new();
133                tokens.append_all(item_mod.attrs.iter());
134                item_mod.vis.to_tokens(&mut tokens);
135                item_mod.struct_token.to_tokens(&mut tokens);
136                item_mod.ident.to_tokens(&mut tokens);
137                tokens
138            }
139            RcssAtRuleConfig::Extend(path) => path.to_token_stream(),
140        };
141        dest.write_str(&format!("@rcss({args});"))
142    }
143}
144
145impl<'i, V: Visitor<'i, RcssAtRuleConfig>> Visit<'i, RcssAtRuleConfig, V> for RcssAtRuleConfig {
146    const CHILD_TYPES: VisitTypes = VisitTypes::empty();
147    fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
148        Ok(())
149    }
150}
151
152#[cfg(test)]
153mod tests {
154
155    use lightningcss::{rules::CssRule, traits::ToCss};
156    use quote::ToTokens;
157
158    use super::RcssAtRuleParser;
159
160    #[test]
161    fn check_at_rule_mod() {
162        let input = r#"
163            @rcss(pub struct MyStruct);
164            
165            .my-class {
166                color: red;
167            }
168            
169        "#;
170        let stylesheet = lightningcss::stylesheet::StyleSheet::parse_with(
171            input,
172            Default::default(),
173            &mut RcssAtRuleParser,
174        )
175        .unwrap();
176        let rule = stylesheet.rules.0.into_iter().next().unwrap();
177
178        match &rule {
179            CssRule::Custom(super::RcssAtRuleConfig::Struct(item_mod)) => {
180                assert_eq!(
181                    item_mod.to_token_stream().to_string(),
182                    "pub struct MyStruct ;"
183                )
184            }
185            _ => unreachable!(),
186        }
187        let output = rule.to_css_string(Default::default()).unwrap();
188        assert_eq!(output, "@rcss(pub struct MyStruct);");
189    }
190
191    #[test]
192    fn check_at_rule_extend() {
193        let input = r#"
194            @rcss(extend ::path::to::my_mod);
195            
196            .my-class {
197                color: red;
198            }
199            
200        "#;
201        let stylesheet = lightningcss::stylesheet::StyleSheet::parse_with(
202            input,
203            Default::default(),
204            &mut RcssAtRuleParser,
205        )
206        .unwrap();
207        let rule = stylesheet.rules.0.into_iter().next().unwrap();
208        match &rule {
209            CssRule::Custom(super::RcssAtRuleConfig::Extend(path)) => {
210                assert_eq!(
211                    path.to_token_stream().to_string(),
212                    ":: path :: to :: my_mod"
213                )
214            }
215            _ => unreachable!(),
216        }
217        let output = rule.to_css_string(Default::default()).unwrap();
218        assert_eq!(output, "@rcss(:: path :: to :: my_mod);");
219    }
220}