mod template;
use litext::litext;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
fn load_registry() {
let out_dir = std::env::var("OUT_DIR").unwrap_or_default();
let path = std::path::Path::new(&out_dir).join("farben_registry.lsv");
if let Ok(content) = std::fs::read_to_string(&path) {
let mut sections = content.splitn(2, "---\n");
let styles_section = sections.next().unwrap_or("");
let prefixes_section = sections.next().unwrap_or("");
for line in styles_section.lines() {
if line.is_empty() {
continue;
}
let (key, value) = line.split_once('=').unwrap();
farben_core::registry::insert_style(
key,
farben_core::ansi::Style::parse(format!("[{value}]"))
.unwrap_or_else(|e| panic!("farben: invalid style in registry '{key}': {e}")),
)
.unwrap_or_else(|e| panic!("farben: invalid style name in registry '{key}': {e}"));
}
for line in prefixes_section.lines() {
if line.is_empty() {
continue;
}
let (key, value) = line.split_once('=').unwrap();
farben_core::registry::set_prefix(key, value)
.unwrap_or_else(|e| panic!("farben: failure while setting prefix '{key}': {e}"));
}
}
}
#[proc_macro]
pub fn color(input: TokenStream) -> TokenStream {
load_registry();
let input = litext!(input as litext::LitStr);
let value = input.value();
let tokens = match farben_core::lexer::tokenize(value) {
Ok(t) => t,
Err(e) => {
let msg = e.to_string();
return comperr::error(input.span(), msg).into();
}
};
let styled = format!("{}\x1b[0m", farben_core::parser::render_forced(tokens));
quote! {
::farben::FarbenStr { styled: #styled }
}
.into()
}
#[proc_macro]
pub fn colorb(input: TokenStream) -> TokenStream {
load_registry();
let input = litext!(input as litext::LitStr);
let value = input.value();
let tokens = match farben_core::lexer::tokenize(value) {
Ok(t) => t,
Err(e) => {
let msg = e.to_string();
return comperr::error(input.span(), msg).into();
}
};
let styled = farben_core::parser::render_forced(tokens);
quote! {
::farben::FarbenStr { styled: #styled }
}
.into()
}
#[proc_macro]
pub fn validate_color(input: TokenStream) -> TokenStream {
load_registry();
let original = input.clone();
let input = litext!(input as litext::LitStr);
let value = input.value();
match farben_core::lexer::tokenize(value) {
Ok(_) => original,
Err(e) => {
let msg = e.to_string();
comperr::error(input.span(), msg).into()
}
}
}
#[cfg(feature = "markdown")]
#[deprecated]
#[proc_macro]
pub fn markdown(input: TokenStream) -> TokenStream {
let input = litext!(input as litext::LitStr);
let value = input.value();
let result = farben_md::renderer::render(&farben_md::lexer::tokenize(value));
quote! { #result }.into()
}
#[proc_macro]
pub fn cformat(input: TokenStream) -> TokenStream {
build_cformat(input, false)
}
#[proc_macro]
pub fn cformatb(input: TokenStream) -> TokenStream {
build_cformat(input, true)
}
fn build_cformat(input: TokenStream, bleed: bool) -> TokenStream {
load_registry();
let mut iter = proc_macro2::TokenStream::from(input).into_iter();
let Some(fmt_tt) = iter.next() else {
return quote! { ::std::string::String::new() }.into();
};
let s = fmt_tt.to_string();
let fmt_str = if s.starts_with('"') && s.ends_with('"') {
let inner = &s[1..s.len() - 1];
inner
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\r", "\r")
.replace("\\\\", "\\")
.replace("\\\"", "\"")
} else {
let msg = "cformat: expected a string literal as first argument";
return comperr::error(fmt_tt.span(), msg).into();
};
let rest: Vec<proc_macro2::TokenTree> = iter.collect();
let args: Vec<TokenStream2> = split_args(rest);
let pieces = match template::split(&fmt_str, bleed) {
Ok(p) => p,
Err(e) => {
return comperr::error(fmt_tt.span(), e.to_string()).into();
}
};
let capacity: usize = pieces
.iter()
.filter_map(|p| {
if let template::Piece::Static { ansi, .. } = p {
Some(ansi.len())
} else {
None
}
})
.sum();
let mut arg_iter = args.iter();
let mut stmts: Vec<TokenStream2> = Vec::new();
for piece in &pieces {
match piece {
template::Piece::Static { ansi, plain } => {
if ansi.is_empty() && plain.is_empty() {
continue;
}
stmts.push(quote! {
__out.push_str(if __color { #ansi } else { #plain });
});
}
template::Piece::Arg(spec) => {
let fmt_spec = format!("{{{spec}}}");
let fmt_lit_str = format!(r#""{fmt_spec}""#);
let fmt_lit: proc_macro2::TokenStream = fmt_lit_str.parse().unwrap();
match arg_iter.next() {
Some(arg) => {
stmts.push(quote! {
::std::fmt::Write::write_fmt(
&mut __out,
::std::format_args!(#fmt_lit, #arg),
).unwrap();
});
}
None => {
stmts.push(quote! {
::std::fmt::Write::write_fmt(
&mut __out,
::std::format_args!(#fmt_lit),
).unwrap();
});
}
}
}
}
}
quote! {
{
use ::std::fmt::Write as _;
let __color = ::farben::color_enabled();
let mut __out = ::std::string::String::with_capacity(#capacity);
#(#stmts)*
__out
}
}
.into()
}
fn split_args(tts: Vec<proc_macro2::TokenTree>) -> Vec<TokenStream2> {
let mut args: Vec<TokenStream2> = Vec::new();
let mut current: Vec<proc_macro2::TokenTree> = Vec::new();
let mut depth = 0usize;
for tt in tts {
match &tt {
proc_macro2::TokenTree::Punct(p) if p.as_char() == ',' && depth == 0 => {
if !current.is_empty() {
args.push(current.drain(..).collect());
}
}
proc_macro2::TokenTree::Group(_) => {
depth += 1;
current.push(tt);
depth -= 1;
}
_ => current.push(tt),
}
}
if !current.is_empty() {
args.push(current.into_iter().collect());
}
args
}