use std::{collections::HashMap, convert::Infallible}; use cssparser::*; use lightningcss::{ declaration::DeclarationBlock, error::PrinterError, printer::Printer, properties::custom::{Token, TokenOrValue}, rules::{style::StyleRule, CssRule, CssRuleList, Location}, selector::{Component, Selector}, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, traits::{AtRuleParser, ToCss}, values::{ color::{CssColor, RGBA}, length::LengthValue, }, vendor_prefix::VendorPrefix, visit_types, visitor::{Visit, VisitTypes, Visitor}, }; fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { filename: args[1].clone(), ..Default::default() }; let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap(); println!("{:?}", stylesheet); let mut style_rules = HashMap::new(); stylesheet .visit(&mut StyleRuleCollector { rules: &mut style_rules, }) .unwrap(); println!("{:?}", style_rules); stylesheet.visit(&mut ApplyVisitor { rules: &style_rules }).unwrap(); let result = stylesheet .to_css(PrinterOptions { targets: Browsers { chrome: Some(100 << 16), ..Browsers::default() } .into(), ..PrinterOptions::default() }) .unwrap(); println!("{}", result.code); } /// An @tailwind directive. #[derive(Debug, Clone)] enum TailwindDirective { Base, Components, Utilities, Variants, } /// A custom at rule prelude. enum Prelude { Tailwind(TailwindDirective), Apply(Vec), } /// A @tailwind rule. #[derive(Debug, Clone)] struct TailwindRule { directive: TailwindDirective, loc: SourceLocation, } /// An @apply rule. #[derive(Debug, Clone)] struct ApplyRule { names: Vec, loc: SourceLocation, } /// A custom at rule. #[derive(Debug, Clone)] enum AtRule { Tailwind(TailwindRule), Apply(ApplyRule), } #[derive(Debug)] struct TailwindAtRuleParser; impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { type Prelude = Prelude; type Error = Infallible; type AtRule = AtRule; fn parse_prelude<'t>( &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, _options: &ParserOptions<'_, 'i>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { let location = input.current_source_location(); let ident = input.expect_ident()?; let directive = match_ignore_ascii_case! { &*ident, "base" => TailwindDirective::Base, "components" => TailwindDirective::Components, "utilities" => TailwindDirective::Utilities, "variants" => TailwindDirective::Variants, _ => return Err(location.new_unexpected_token_error( cssparser::Token::Ident(ident.clone()) )) }; Ok(Prelude::Tailwind(directive)) }, "apply" => { let mut names = Vec::new(); loop { if let Ok(name) = input.try_parse(|input| input.expect_ident_cloned()) { names.push(name.as_ref().into()); } else { break } } Ok(Prelude::Apply(names)) }, _ => Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) } } fn rule_without_block( &mut self, prelude: Self::Prelude, start: &ParserState, _options: &ParserOptions<'_, 'i>, _is_nested: bool, ) -> Result { let loc = start.source_location(); match prelude { Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })), Prelude::Apply(names) => Ok(AtRule::Apply(ApplyRule { names, loc })), } } } struct StyleRuleCollector<'i, 'a> { rules: &'a mut HashMap>, } impl<'i, 'a> Visitor<'i, AtRule> for StyleRuleCollector<'i, 'a> { type Error = Infallible; fn visit_types(&self) -> VisitTypes { VisitTypes::RULES } fn visit_rule(&mut self, rule: &mut lightningcss::rules::CssRule<'i, AtRule>) -> Result<(), Self::Error> { match rule { CssRule::Style(rule) => { for selector in rule.selectors.0.iter() { if selector.len() != 1 { continue; // TODO } for component in selector.iter_raw_match_order() { match component { Component::Class(name) => { self.rules.insert(name.0.to_string(), rule.declarations.clone()); } _ => {} } } } } _ => {} } rule.visit_children(self) } } struct ApplyVisitor<'a, 'i> { rules: &'a HashMap>, } impl<'a, 'i> Visitor<'i, AtRule> for ApplyVisitor<'a, 'i> { type Error = Infallible; fn visit_types(&self) -> VisitTypes { visit_types!(RULES | COLORS | LENGTHS | DASHED_IDENTS | SELECTORS | TOKENS) } fn visit_rule(&mut self, rule: &mut CssRule<'i, AtRule>) -> Result<(), Self::Error> { // Replace @apply rule with nested style rule. if let CssRule::Custom(AtRule::Apply(apply)) = rule { let mut declarations = DeclarationBlock::new(); for name in &apply.names { let Some(applied) = self.rules.get(name) else { continue; }; declarations .important_declarations .extend(applied.important_declarations.iter().cloned()); declarations.declarations.extend(applied.declarations.iter().cloned()); } *rule = CssRule::Style(StyleRule { selectors: Component::Nesting.into(), vendor_prefix: VendorPrefix::None, declarations, rules: CssRuleList(vec![]), loc: Location { source_index: 0, line: apply.loc.line, column: apply.loc.column, }, }) } rule.visit_children(self) } fn visit_url(&mut self, url: &mut lightningcss::values::url::Url<'i>) -> Result<(), Self::Error> { url.url = format!("https://mywebsite.com/{}", url.url).into(); Ok(()) } fn visit_color(&mut self, color: &mut lightningcss::values::color::CssColor) -> Result<(), Self::Error> { *color = color.to_lab().unwrap(); Ok(()) } fn visit_length(&mut self, length: &mut lightningcss::values::length::LengthValue) -> Result<(), Self::Error> { match length { LengthValue::Px(px) => *length = LengthValue::Rem(*px / 16.0), _ => {} } Ok(()) } fn visit_dashed_ident( &mut self, ident: &mut lightningcss::values::ident::DashedIdent, ) -> Result<(), Self::Error> { ident.0 = format!("--tw-{}", &ident.0[2..]).into(); Ok(()) } fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> { for c in selector.iter_mut_raw_match_order() { match c { Component::Class(c) => { *c = format!("tw-{}", c).into(); } _ => {} } } Ok(()) } fn visit_token(&mut self, token: &mut TokenOrValue<'i>) -> Result<(), Self::Error> { match token { TokenOrValue::Function(f) if f.name == "theme" => match f.arguments.0.first() { Some(TokenOrValue::Token(Token::String(s))) => match s.as_ref() { "blue-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(0, 0, 255, 1.0))), "red-500" => *token = TokenOrValue::Color(CssColor::RGBA(RGBA::new(255, 0, 0, 1.0))), _ => {} }, _ => {} }, _ => {} } token.visit_children(self) } } #[cfg(feature = "visitor")] impl<'i, V: Visitor<'i, AtRule>> Visit<'i, AtRule, V> for AtRule { const CHILD_TYPES: VisitTypes = VisitTypes::empty(); fn visit_children(&mut self, _: &mut V) -> Result<(), V::Error> { Ok(()) } } impl ToCss for AtRule { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> { match self { AtRule::Tailwind(rule) => { let _ = rule.loc; // TODO: source maps let directive = match rule.directive { TailwindDirective::Base => "TAILWIND BASE HERE", TailwindDirective::Components => "TAILWIND COMPONENTS HERE", TailwindDirective::Utilities => "TAILWIND UTILITIES HERE", TailwindDirective::Variants => "TAILWIND VARIANTS HERE", }; dest.write_str(directive) } AtRule::Apply(_) => Ok(()), } } }