use nom::{
branch::alt,
bytes::complete::{tag, take_until, take_while1},
character::complete::{digit1, multispace0, multispace1},
combinator::{all_consuming, not, opt, recognize},
multi::separated_list0,
number,
sequence::{preceded, tuple},
IResult,
};
use std::collections::HashSet;
use syn::{parse_macro_input, LitStr};
mod config;
mod plugins;
mod tailwind;
use tailwind::{
colorful::COLORFUL_BASECLASSES, lengthy::LENGTHY, modifiers::get_modifiers,
tailwind_config::CustomisableClasses, valid_baseclass_names::VALID_BASECLASS_NAMES,
};
use config::{get_classes, noconfig::UNCONFIGURABLE, read_tailwind_config};
use proc_macro::TokenStream;
use tailwind::signable::SIGNABLES;
fn setup(input: &LitStr) -> Result<(Vec<String>, Vec<String>), TokenStream> {
let config = &(match read_tailwind_config() {
Ok(config) => config,
Err(e) => {
return Err(syn::Error::new_spanned(
input,
format!("Error reading Tailwind config: {}", e),
)
.to_compile_error()
.into());
}
});
let modifiers = get_modifiers(config);
let valid_class_names = get_classes(config);
let is_unconfigurable = |classes: &CustomisableClasses, action_type_str: &str| {
serde_json::to_value(classes)
.expect("Unable to convert to value")
.as_object()
.expect("Unable to convert to object")
.iter()
.any(|(key, value)| {
if UNCONFIGURABLE.contains(&key.as_str()) && !value.is_null() {
panic!("You cannot {action_type_str} the key: {key} in tailwind.config.json",);
}
false
})
};
is_unconfigurable(&config.theme.overrides, "override");
is_unconfigurable(&config.theme.extend, "extend");
Ok((modifiers, valid_class_names))
}
fn get_classes_straight() -> HashSet<String> {
HashSet::from_iter(get_classes(
&read_tailwind_config().expect("Problem getting classes"),
))
}
fn is_valid_classname(class_name: &str) -> bool {
get_classes_straight().contains(class_name)
}
fn is_valid_modifier(modifier: &str) -> bool {
let modifiers: HashSet<String> = HashSet::from_iter(get_modifiers(
&read_tailwind_config().expect("Problem getting modifiers"),
));
modifiers.contains(modifier)
}
fn parse_predefined_tw_classname(input: &str) -> IResult<&str, ()> {
let (input, class_name) = recognize(|i| {
nom::bytes::complete::is_a(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-./",
)(i)
})(input)?;
let is_signable = SIGNABLES.iter().any(|s| {
class_name
.strip_prefix('-')
.unwrap_or(class_name)
.starts_with(s)
});
if is_signable && is_valid_classname(class_name.strip_prefix('-').unwrap_or(class_name))
|| !is_signable && is_valid_classname(class_name)
{
Ok((input, ()))
} else {
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn is_lengthy_classname(class_name: &str) -> bool {
LENGTHY.contains(&class_name.strip_prefix('-').unwrap_or(class_name))
}
fn float_strict(input: &str) -> IResult<&str, f64> {
let (input, number) = recognize(tuple((
opt(alt((tag("-"), tag("+")))),
digit1,
opt(preceded(tag("."), digit1)),
opt(tuple((
alt((tag("e"), tag("E"))),
opt(alt((tag("-"), tag("+")))),
digit1,
))),
)))(input)?;
let float_val: f64 = number.parse().unwrap();
Ok((input, float_val))
}
fn parse_length_unit(input: &str) -> IResult<&str, String> {
let (input, number) = float_strict(input)?;
let (input, unit) = {
alt((
tag("px"),
tag("em"),
tag("rem"),
tag("%"),
tag("cm"),
tag("mm"),
tag("in"),
tag("pt"),
tag("pc"),
tag("vh"),
tag("vw"),
tag("vmin"),
tag("vmax"),
tag(""),
))
}(input)?;
Ok((input, format!("{}{}", number, unit)))
}
fn lengthy_arbitrary_classname(input: &str) -> IResult<&str, ()> {
let (input, class_name) = take_until("-[")(input)?;
let (input, _) = if is_lengthy_classname(class_name) {
Ok((input, ()))
} else {
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}?;
let (input, _) = tag("-")(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = parse_length_unit(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn parse_hex_color(input: &str) -> IResult<&str, String> {
let (input, _) = tag("#")(input)?;
let (input, color) = take_while1(|c: char| c.is_ascii_hexdigit())(input)?;
let (input, _) = if color.chars().count() == 3 || color.chars().count() == 6 {
Ok((input, ()))
} else {
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}?;
let color = format!("#{}", color);
Ok((input, color))
}
fn parse_u8(input: &str) -> IResult<&str, u8> {
let (input, num) = number::complete::double(input)?;
let input = match num as u32 {
0..=255 => input,
_ => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}
};
Ok((input, num as u8))
}
fn parse_rgb_color(input: &str) -> IResult<&str, String> {
let (input, _) = tag("rgb(")(input)?;
let (input, r) = parse_u8(input)?;
let (input, _) = alt((tag(","), tag("_")))(input)?;
let (input, g) = parse_u8(input)?;
let (input, _) = alt((tag(","), tag("_")))(input)?;
let (input, b) = parse_u8(input)?;
let (input, _) = tag(")")(input)?;
let color = format!("rgb({}, {}, {})", r, g, b);
Ok((input, color))
}
fn parse_rgba_color(input: &str) -> IResult<&str, String> {
let (input, _) = tag("rgba(")(input)?;
let (input, r) = parse_u8(input)?;
let (input, _) = alt((tag(","), tag("_")))(input)?;
let (input, g) = parse_u8(input)?;
let (input, _) = alt((tag(","), tag("_")))(input)?;
let (input, b) = parse_u8(input)?;
let (input, _) = alt((tag(","), tag("_")))(input)?;
let (input, a) = number::complete::double(input)?;
let (input, _) = tag(")")(input)?;
let color = format!("rgba({}, {}, {}, {})", r, g, b, a);
Ok((input, color))
}
fn is_colorful_baseclass(class_name: &str) -> bool {
COLORFUL_BASECLASSES.contains(&class_name)
}
fn colorful_arbitrary_baseclass(input: &str) -> IResult<&str, ()> {
let (input, class_name) = take_until("-[")(input)?;
let (input, _) = if is_colorful_baseclass(class_name) {
Ok((input, ()))
} else {
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}?;
let (input, _) = tag("-")(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = alt((parse_hex_color, parse_rgb_color, parse_rgba_color))(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn kv_pair_classname(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("[")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag(":")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn arbitrary_content(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("content-['")(input)?;
let (input, _) = take_until("']")(input)?;
let (input, _) = tag("']")(input)?;
Ok((input, ()))
}
fn arbitrary_with_arrow(input: &str) -> IResult<&str, ()> {
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = alt((tag(">"), tag("<")))(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn predefined_colorful_opacity(input: &str) -> IResult<&str, ()> {
let input = if COLORFUL_BASECLASSES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?;
let (input, _) = tag("/")(input)?;
let (input, num) = number::complete::double(input)?;
let input = match num as u8 {
0..=100 => input,
_ => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}
};
Ok((input, ()))
}
fn arbitrary_opacity(input: &str) -> IResult<&str, ()> {
let input = if COLORFUL_BASECLASSES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?;
let (input, _) = tag("/")(input)?;
let (input, _) = tag("[")(input)?;
let (input, num) = number::complete::double(input)?;
let input = match num as u8 {
0..=100 => input,
_ => {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}
};
let (input, _) = opt(tag("%"))(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn bg_arbitrary_url(input: &str) -> IResult<&str, ()> {
let input = if COLORFUL_BASECLASSES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = tag("url('")(input)?;
let (input, _) = take_until("')")(input)?;
let (input, _) = tag("')")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn arbitrary_css_value(input: &str) -> IResult<&str, ()> {
let (input, base_class) = take_until("-[")(input)?;
let input = if VALID_BASECLASS_NAMES
.iter()
.any(|cb| base_class.trim().eq(*cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
base_class,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = tag("-[")(input)?;
let (input, _) = not(alt((
tag("--"),
tag("var(--"),
)))(input)?;
let (input, _) = take_while1(|char| is_ident_char(char) && char != '(')(input)?;
let (input, _) = tag("(")(input)?;
let (input, _) = take_until(")]")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn arbitrary_css_var(input: &str) -> IResult<&str, ()> {
let input = if VALID_BASECLASS_NAMES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = tag("--")(input)?;
let (input, _) = take_while1(|char| is_ident_char(char) && char != ']')(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn arbitrary_css_var2(input: &str) -> IResult<&str, ()> {
let input = if VALID_BASECLASS_NAMES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = tag("var(--")(input)?;
let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?;
let (input, _) = tag(")]")(input)?;
Ok((input, ()))
}
fn arbitrary_css_var3(input: &str) -> IResult<&str, ()> {
let input = if VALID_BASECLASS_NAMES
.iter()
.any(|cb| input.trim().starts_with(cb))
{
input
} else {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
};
let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = take_while1(|char| is_ident_char(char) && char != ':')(input)?;
let (input, _) = tag(":")(input)?;
let (input, _) = tag("var(--")(input)?;
let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?;
let (input, _) = tag(")]")(input)?;
Ok((input, ()))
}
fn arbitrary_group_classname(input: &str) -> IResult<&str, ()> {
let (input, _) = alt((tag("group"),))(input)?;
let (input, _) = tag("/")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
Ok((input, ()))
}
fn parse_single_tw_classname(input: &str) -> IResult<&str, ()> {
alt((
bg_arbitrary_url,
predefined_colorful_opacity,
arbitrary_group_classname,
arbitrary_opacity,
parse_predefined_tw_classname,
kv_pair_classname,
lengthy_arbitrary_classname,
colorful_arbitrary_baseclass,
arbitrary_content,
arbitrary_with_arrow,
arbitrary_css_var,
arbitrary_css_var2,
arbitrary_css_var3,
arbitrary_css_value,
))(input)
}
fn predefined_modifier(input: &str) -> IResult<&str, ()> {
let (input, modifier) = recognize(|i| {
nom::bytes::complete::is_a(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-",
)(i)
})(input)?;
if is_valid_modifier(modifier) {
Ok((input, ()))
} else {
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)))
}
}
fn predefined_special_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = alt((
tuple((tag("peer-"), predefined_modifier)),
tuple((tag("group-"), predefined_modifier)),
))(input)?;
Ok((input, ()))
}
fn arbitrary_front_selector_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("[&")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn arbitrary_back_selector_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
let (input, _) = tag("-[")(input)?;
let (input, _) = take_until("&]")(input)?;
let (input, _) = tag("&]")(input)?;
Ok((input, ()))
}
fn arbitrary_at_supports_rule_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("[@supports(")(input)?;
let (input, _) = take_until(")")(input)?;
let (input, _) = tag(")]")(input)?;
Ok((input, ()))
}
fn arbitrary_at_media_rule_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("[@media(")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn group_peer_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = alt((
tuple((tag("group-"), predefined_modifier)),
tuple((tag("peer-"), predefined_modifier)),
))(input)?;
let (input, _) = tag("/")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
Ok((input, ()))
}
fn group_modifier_selector(input: &str) -> IResult<&str, ()> {
let (input, _) = alt((tag("group"), tag("peer")))(input)?;
let (input, _) = tag("-[")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn supports_arbitrary(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("supports-[")(input)?;
let (input, _) = take_until("]")(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn aria_or_data_arbitrary(input: &str) -> IResult<&str, ()> {
let (input, _) = opt(tag("group-"))(input)?;
let (input, _) = alt((tag("aria-["), tag("data-[")))(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag("=")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag("]")(input)?;
let (input, _) = opt(tuple((tag("/"), take_while1(is_ident_char))))(input)?;
Ok((input, ()))
}
fn data_arbitrary(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("data-[")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag("=")(input)?;
let (input, _) = take_while1(is_ident_char)(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn min_max_arbitrary_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = alt((tag("min-"), tag("max-")))(input)?;
let (input, _) = tag("[")(input)?;
let (input, _) = parse_length_unit(input)?;
let (input, _) = tag("]")(input)?;
Ok((input, ()))
}
fn wildcard_modifier(input: &str) -> IResult<&str, ()> {
let (input, _) = tag("*")(input)?;
Ok((input, ()))
}
fn modifier(input: &str) -> IResult<&str, ()> {
alt((
group_modifier_selector,
group_peer_modifier,
predefined_special_modifier,
arbitrary_front_selector_modifier,
arbitrary_back_selector_modifier,
arbitrary_at_supports_rule_modifier,
arbitrary_at_media_rule_modifier,
predefined_modifier,
supports_arbitrary,
aria_or_data_arbitrary,
data_arbitrary,
min_max_arbitrary_modifier,
wildcard_modifier,
))(input)
}
fn modifiers_chained(input: &str) -> IResult<&str, ()> {
let (input, _modifiers) = separated_list0(tag(":"), modifier)(input)?;
Ok((input, ()))
}
fn parse_tw_full_classname(input: &str) -> IResult<&str, Vec<&str>> {
let (input, _class_names) = tuple((
opt(tuple((modifiers_chained, tag(":")))),
parse_single_tw_classname,
))(input)?;
Ok((input, vec![]))
}
fn parse_class_names(input: &str) -> IResult<&str, Vec<&str>> {
let (input, _) = multispace0(input)?;
let (input, _class_names) = separated_list0(multispace1, parse_tw_full_classname)(input)?;
let (input, _) = multispace0(input)?;
Ok((input, vec![]))
}
fn parse_top(input: &str) -> IResult<&str, Vec<&str>> {
all_consuming(parse_class_names)(input)
}
fn parse_single_top(input: &str) -> IResult<&str, Vec<&str>> {
all_consuming(parse_tw_full_classname)(input)
}
#[proc_macro]
pub fn twust_many_classes(raw_input: TokenStream) -> TokenStream {
let r_input = raw_input.clone();
let input_original = parse_macro_input!(r_input as LitStr);
let (_modifiers, _valid_class_names) = match setup(&input_original) {
Ok(value) => value,
Err(value) => {
return syn::Error::new_spanned(input_original, value)
.to_compile_error()
.into()
}
};
let full_classnames = input_original.value();
let (_input, _class_names) = match parse_top(&full_classnames) {
Ok(value) => value,
Err(value) => {
return syn::Error::new_spanned(input_original, value)
.to_compile_error()
.into()
}
};
quote::quote! {
#input_original
}
.into()
}
#[proc_macro]
pub fn twust_one_class(raw_input: TokenStream) -> TokenStream {
let r_input = raw_input.clone();
let input_original = parse_macro_input!(r_input as LitStr);
let (_modifiers, _valid_class_names) = match setup(&input_original) {
Ok(value) => value,
Err(value) => {
return syn::Error::new_spanned(input_original, value)
.to_compile_error()
.into()
}
};
let full_classnames = input_original.value();
let (_input, _class_names) = match parse_single_top(&full_classnames) {
Ok(value) => value,
Err(value) => {
return syn::Error::new_spanned(input_original, value)
.to_compile_error()
.into()
}
};
quote::quote! {
#input_original
}
.into()
}