blob: 9c091e6b0288f350fcc1acb41119e0b74f570ef2 [file] [log] [blame] [edit]
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(()),
}
}
}