rcss_core/
rcss_at_rule.rs1use 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 Token).to_tokens(&mut tokens);
45
46 let mut result = syn::parse2::<ItemStruct>(tokens)?;
47 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 }
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 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}