Skip to main content

custom_at_rule/
custom_at_rule.rs

1use std::{collections::HashMap, convert::Infallible};
2
3use cssparser::*;
4use lightningcss::{
5  declaration::DeclarationBlock,
6  error::PrinterError,
7  printer::Printer,
8  properties::custom::{Token, TokenOrValue},
9  rules::{style::StyleRule, CssRule, CssRuleList, Location},
10  selector::{Component, Selector},
11  stylesheet::{ParserOptions, PrinterOptions, StyleSheet},
12  targets::Browsers,
13  traits::{AtRuleParser, ToCss},
14  values::{
15    color::{CssColor, RGBA},
16    length::LengthValue,
17  },
18  vendor_prefix::VendorPrefix,
19  visit_types,
20  visitor::{Visit, VisitTypes, Visitor},
21};
22
23fn main() {
24  let args: Vec<String> = std::env::args().collect();
25  let source = std::fs::read_to_string(&args[1]).unwrap();
26  let opts = ParserOptions {
27    filename: args[1].clone(),
28    ..Default::default()
29  };
30
31  let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap();
32
33  println!("{:?}", stylesheet);
34
35  let mut style_rules = HashMap::new();
36  stylesheet
37    .visit(&mut StyleRuleCollector {
38      rules: &mut style_rules,
39    })
40    .unwrap();
41  println!("{:?}", style_rules);
42  stylesheet.visit(&mut ApplyVisitor { rules: &style_rules }).unwrap();
43
44  let result = stylesheet
45    .to_css(PrinterOptions {
46      targets: Browsers {
47        chrome: Some(100 << 16),
48        ..Browsers::default()
49      }
50      .into(),
51      ..PrinterOptions::default()
52    })
53    .unwrap();
54  println!("{}", result.code);
55}
56
57/// An @tailwind directive.
58#[derive(Debug, Clone)]
59enum TailwindDirective {
60  Base,
61  Components,
62  Utilities,
63  Variants,
64}
65
66/// A custom at rule prelude.
67enum Prelude {
68  Tailwind(TailwindDirective),
69  Apply(Vec<String>),
70}
71
72/// A @tailwind rule.
73#[derive(Debug, Clone)]
74struct TailwindRule {
75  directive: TailwindDirective,
76  loc: SourceLocation,
77}
78
79/// An @apply rule.
80#[derive(Debug, Clone)]
81struct ApplyRule {
82  names: Vec<String>,
83  loc: SourceLocation,
84}
85
86/// A custom at rule.
87#[derive(Debug, Clone)]
88enum AtRule {
89  Tailwind(TailwindRule),
90  Apply(ApplyRule),
91}
92
93#[derive(Debug)]
94struct TailwindAtRuleParser;
95impl<'i> AtRuleParser<'i> for TailwindAtRuleParser {
96  type Prelude = Prelude;
97  type Error = Infallible;
98  type AtRule = AtRule;
99
100  fn parse_prelude<'t>(
101    &mut self,
102    name: CowRcStr<'i>,
103    input: &mut Parser<'i, 't>,
104    _options: &ParserOptions<'i>,
105  ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
106    match_ignore_ascii_case! {&*name,
107      "tailwind" => {
108        let location = input.current_source_location();
109        let ident = input.expect_ident()?;
110        let directive = match_ignore_ascii_case! { &*ident,
111          "base" => TailwindDirective::Base,
112          "components" => TailwindDirective::Components,
113          "utilities" => TailwindDirective::Utilities,
114          "variants" => TailwindDirective::Variants,
115          _ => return Err(location.new_unexpected_token_error(
116            cssparser::Token::Ident(ident.clone())
117          ))
118        };
119        Ok(Prelude::Tailwind(directive))
120      },
121      "apply" => {
122        let mut names = Vec::new();
123        loop {
124          if let Ok(name) = input.try_parse(|input| input.expect_ident_cloned()) {
125            names.push(name.as_ref().into());
126          } else {
127            break
128          }
129        }
130
131        Ok(Prelude::Apply(names))
132      },
133      _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name)))
134    }
135  }
136
137  fn rule_without_block(
138    &mut self,
139    prelude: Self::Prelude,
140    start: &ParserState,
141    _options: &ParserOptions<'i>,
142    _is_nested: bool,
143  ) -> Result<Self::AtRule, ()> {
144    let loc = start.source_location();
145    match prelude {
146      Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })),
147      Prelude::Apply(names) => Ok(AtRule::Apply(ApplyRule { names, loc })),
148    }
149  }
150}
151
152struct StyleRuleCollector<'i, 'a> {
153  rules: &'a mut HashMap<String, DeclarationBlock<'i>>,
154}
155
156impl<'i, 'a> Visitor<'i, AtRule> for StyleRuleCollector<'i, 'a> {
157  type Error = Infallible;
158
159  fn visit_types(&self) -> VisitTypes {
160    VisitTypes::RULES
161  }
162
163  fn visit_rule(&mut self, rule: &mut lightningcss::rules::CssRule<'i, AtRule>) -> Result<(), Self::Error> {
164    match rule {
165      CssRule::Style(rule) => {
166        for selector in rule.selectors.0.iter() {
167          if selector.len() != 1 {
168            continue; // TODO
169          }
170          for component in selector.iter_raw_match_order() {
171            match component {
172              Component::Class(name) => {
173                self.rules.insert(name.0.to_string(), rule.declarations.clone());
174              }
175              _ => {}
176            }
177          }
178        }
179      }
180      _ => {}
181    }
182
183    rule.visit_children(self)
184  }
185}
186
187struct ApplyVisitor<'a, 'i> {
188  rules: &'a HashMap<String, DeclarationBlock<'i>>,
189}
190
191impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> {
192  type Error = Infallible;
193
194  fn visit_types(&self) -> VisitTypes {
195    visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS)
196  }
197
198  fn visit_rule(&mut self, rule: &mut CssRule<'i, AtRule>) -> Result<(), Self::Error> {
199    // Replace @apply rule with nested style rule.
200    if let CssRule::Custom(AtRule::Apply(apply)) = rule {
201      let mut declarations = DeclarationBlock::new();
202      for name in &apply.names {
203        let Some(applied) = self.rules.get(name) else {
204          continue;
205        };
206        declarations
207          .important_declarations
208          .extend(applied.important_declarations.iter().cloned());
209        declarations.declarations.extend(applied.declarations.iter().cloned());
210      }
211      *rule = CssRule::Style(StyleRule {
212        selectors: Component::Nesting.into(),
213        vendor_prefix: VendorPrefix::None,
214        declarations,
215        rules: CssRuleList(vec![]),
216        loc: Location {
217          source_index: 0,
218          line: apply.loc.line,
219          column: apply.loc.column,
220        },
221      })
222    }
223
224    rule.visit_children(self)
225  }
226
227  fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> {
228    url.url = format!("https://mywebsite.com/{}", url.url).into();
229    Ok(())
230  }
231
232  fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> {
233    *color = color.to_lab().unwrap();
234    Ok(())
235  }
236
237  fn visit_length(&mut self, length: &mut lightningcss::values::length::LengthValue) -> Result<(), Self::Error> {
238    match length {
239      LengthValue::Px(px) => *length = LengthValue::Rem(*px / 16.0),
240      _ => {}
241    }
242
243    Ok(())
244  }
245
246  fn visit_dashed_ident(
247    &mut self,
248    ident: &mut lightningcss::values::ident::DashedIdent,
249  ) -> Result<(), Self::Error> {
250    ident.0 = format!("--tw-{}", &ident.0[2..]).into();
251    Ok(())
252  }
253
254  fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> {
255    for c in selector.iter_mut_raw_match_order() {
256      match c {
257        Component::Class(c) => {
258          *c = format!("tw-{}", c).into();
259        }
260        _ => {}
261      }
262    }
263
264    Ok(())
265  }
266
267  fn visit_token(&mut self, token: &mut TokenOrValue<'i>) -> Result<(), Self::Error> {
268    match token {
269      TokenOrValue::Function(f) if f.name == "theme" => match f.arguments.0.first() {
270        Some(TokenOrValue::Token(Token::String(s))) => match s.as_ref() {
271          "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 1.0))),
272          "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 1.0))),
273          _ => {}
274        },
275        _ => {}
276      },
277      _ => {}
278    }
279
280    token.visit_children(self)
281  }
282}
283
284#[cfg(feature = "visitor")]
285impl<'i, V: Visitor<'i, AtRule>> Visit<'i, AtRule, V> for AtRule {
286  const CHILD_TYPES: VisitTypes = VisitTypes::empty();
287
288  fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> {
289    Ok(())
290  }
291}
292
293impl ToCss for AtRule {
294  fn to_css<W: std::fmt::Write>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> {
295    match self {
296      AtRule::Tailwind(rule) => {
297        let _ = rule.loc; // TODO: source maps
298        let directive = match rule.directive {
299          TailwindDirective::Base => "TAILWIND BASE HERE",
300          TailwindDirective::Components => "TAILWIND COMPONENTS HERE",
301          TailwindDirective::Utilities => "TAILWIND UTILITIES HERE",
302          TailwindDirective::Variants => "TAILWIND VARIANTS HERE",
303        };
304        dest.write_str(directive)
305      }
306      AtRule::Apply(_) => Ok(()),
307    }
308  }
309}