#![allow(clippy::module_name_repetitions)]
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, Expr, Ident, LitInt, LitStr, Token,
};
struct StyleArgs {
styles: Vec<StyleEntry>,
expr: Expr,
}
enum StyleEntry {
Flag(Ident), KeyValue(Ident, ColorSpec), }
enum ColorSpec {
Basic(Ident), Color256(LitInt), Rgb(LitInt, LitInt, LitInt), Hex(LitStr), }
impl Parse for StyleArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let expr: Expr = input.parse()?;
let mut styles = Vec::new();
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.peek(Ident) && input.peek2(Token![=]) {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let color_spec = if input.peek(Ident) {
let ident: Ident = input.parse()?;
let ident_str = ident.to_string();
if ident_str == "Color256" && input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let n: LitInt = content.parse()?;
ColorSpec::Color256(n)
} else if ident_str == "Rgb" && input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let r: LitInt = content.parse()?;
content.parse::<Token![,]>()?;
let g: LitInt = content.parse()?;
content.parse::<Token![,]>()?;
let b: LitInt = content.parse()?;
ColorSpec::Rgb(r, g, b)
} else {
ColorSpec::Basic(ident)
}
} else if input.peek(LitStr) {
let hex: LitStr = input.parse()?;
ColorSpec::Hex(hex)
} else {
return Err(input.error(
"Expected color specification (Red, Color256(n), Rgb(r,g,b), or \"#hex\")",
));
};
styles.push(StyleEntry::KeyValue(key, color_spec));
} else if input.peek(Ident) {
let flag: Ident = input.parse()?;
styles.push(StyleEntry::Flag(flag));
} else {
return Err(input.error("Expected identifier or key=value style entry"));
}
}
Ok(Self { styles, expr })
}
}
pub fn styled_impl(input: TokenStream) -> TokenStream {
let StyleArgs { styles, expr } = parse_macro_input!(input as StyleArgs);
let mut expr_tokens = quote! { (#expr).style() };
for style in styles {
match style {
StyleEntry::Flag(flag) => {
expr_tokens = quote! { #expr_tokens.#flag() };
}
StyleEntry::KeyValue(key, color_spec) => {
match color_spec {
ColorSpec::Basic(ident) => {
expr_tokens = quote! { #expr_tokens.#key(Color::#ident) };
}
ColorSpec::Color256(n) => {
expr_tokens = quote! { #expr_tokens.#key(Color::Color256(#n)) };
}
ColorSpec::Rgb(r, g, b) => {
expr_tokens = quote! { #expr_tokens.#key(Color::Rgb(#r, #g, #b)) };
}
ColorSpec::Hex(hex_str) => {
let hex = hex_str.value();
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return TokenStream::from(quote! {
compile_error!("Hex color must be 6 characters (e.g., \"#ff0000\")")
});
}
match (
u8::from_str_radix(&hex[0..2], 16),
u8::from_str_radix(&hex[2..4], 16),
u8::from_str_radix(&hex[4..6], 16),
) {
(Ok(r), Ok(g), Ok(b)) => {
expr_tokens = quote! { #expr_tokens.#key(Color::Rgb(#r, #g, #b)) };
}
_ => {
return TokenStream::from(quote! {
compile_error!("Invalid hex color format. Use \"#rrggbb\" format.")
});
}
}
}
}
}
}
}
TokenStream::from(quote! { #expr_tokens })
}