lightningcss 1.0.0-alpha.71

A CSS parser, transformer, and minifier
Documentation
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<String> = 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<String>),
}

/// A @tailwind rule.
#[derive(Debug, Clone)]
struct TailwindRule {
  directive: TailwindDirective,
  loc: SourceLocation,
}

/// An @apply rule.
#[derive(Debug, Clone)]
struct ApplyRule {
  names: Vec<String>,
  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<Self::Prelude, ParseError<'i, Self::Error>> {
    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<Self::AtRule, ()> {
    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<String, DeclarationBlock<'i>>,
}

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<String, DeclarationBlock<'i>>,
}

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<W: std::fmt::Write>(&self, dest: &mut Printer<W>) -> 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(()),
    }
  }
}