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, .. } => {
if ansi.is_empty() {
continue;
}
stmts.push(quote! {
::std::fmt::Write::write_fmt(
&mut __out,
::std::format_args!(
"{}",
::farben::FarbenStr { styled: #ansi },
),
).unwrap();
});
}
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 mut __out = ::std::string::String::with_capacity(#capacity);
#(#stmts)*
__out
}
}
.into()
}
#[proc_macro]
pub fn compile_cprint(input: TokenStream) -> TokenStream {
load_registry();
let input_ts = proc_macro2::TokenStream::from(input);
let Some(fmt_tt) = input_ts.clone().into_iter().next() else {
return quote! { ::farben::FarbenStr { styled: "" } }.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 = "compile_cprint: expected a string literal";
return comperr::error(fmt_tt.span(), msg).into();
};
let has_args = {
let mut chars = fmt_str.chars().peekable();
let mut found = false;
while let Some(c) = chars.next() {
if c == '{' {
if let Some('{') = chars.peek() {
chars.next();
} else {
found = true;
break;
}
}
}
found
};
if !has_args {
let tokens = match farben_core::lexer::tokenize(&fmt_str) {
Ok(t) => t,
Err(e) => {
return comperr::error(fmt_tt.span(), e.to_string()).into();
}
};
let styled = format!("{}\x1b[0m", farben_core::parser::render_forced(tokens));
return quote! {
::farben::FarbenStr { styled: #styled }
}
.into();
}
if let Err(e) = farben_core::lexer::tokenize(&fmt_str) {
return comperr::error(fmt_tt.span(), e.to_string()).into();
}
quote! {
::farben::color_runtime(
::std::format!(#fmt_tt),
false,
)
}
.into()
}
#[proc_macro]
pub fn compile_cprintb(input: TokenStream) -> TokenStream {
load_registry();
let input_ts = proc_macro2::TokenStream::from(input);
let Some(fmt_tt) = input_ts.clone().into_iter().next() else {
return quote! { ::farben::FarbenStr { styled: "" } }.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 = "compile_cprintb: expected a string literal";
return comperr::error(fmt_tt.span(), msg).into();
};
let mut chars = fmt_str.chars().peekable();
let mut has_args = false;
while let Some(c) = chars.next() {
if c == '{' {
if let Some('{') = chars.peek() {
chars.next();
} else {
has_args = true;
break;
}
}
}
if !has_args {
let tokens = match farben_core::lexer::tokenize(&fmt_str) {
Ok(t) => t,
Err(e) => {
return comperr::error(fmt_tt.span(), e.to_string()).into();
}
};
let styled = farben_core::parser::render_forced(tokens);
return quote! {
::farben::FarbenStr { styled: #styled }
}
.into();
}
if let Err(e) = farben_core::lexer::tokenize(&fmt_str) {
return comperr::error(fmt_tt.span(), e.to_string()).into();
}
quote! {
::farben::color_runtime(
::std::format!(#fmt_tt),
true,
)
}
.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
}