farben-macros 0.7.0-beta.2

Procedural macros for Farben
Documentation
//! Procedural macros for the Farben terminal styling library.
//!
//! Provides compile-time processing of farben markup and inline markdown,
//! baking the final ANSI-escaped strings directly into the binary with zero
//! runtime overhead. All macros in this crate are re-exported through
//! `farben` and should not be used directly in most cases.
//!
//! ## Features
//!
//! - `markdown`: enables `markdown!` and [`colorb!`] for compile-time
//!   markdown rendering via `farben_md`.

use litext::litext;
use proc_macro::TokenStream;
use quote::quote;

/// Reads `farben_registry.lsv` from `OUT_DIR` and pre-populates the compile-time registry.
///
/// Calls `insert_style` for each style entry and `set_prefix` for each prefix entry
/// written by the build script from `.frb` config files. Called at the start of each
/// proc macro invocation. If the file does not exist, the function returns silently.
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}"));
        }
    }
}

/// Parses and colorizes a farben markup string at compile time.
///
/// Tokenizes and renders the input at compile time, emitting the final ANSI-escaped
/// string as a string literal baked into the binary. Invalid markup produces a
/// compiler error at the call site.
///
/// # Examples
///
/// ```ignore
/// use farben_macros::color;
/// println!("{}", color!("[bold red]Hello!"));
/// ```
#[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));
    let plain = farben_core::strip::strip_markup(input.value());
    quote! {
        ::farben::FarbenStr { styled: #styled, plain: #plain }
    }
    .into()
}

/// Parses and colorizes a farben markup string at compile time, without appending a
/// trailing SGR reset.
///
/// Behaves identically to [`color!`] except the final `\x1b[0m` reset is omitted.
/// Styles applied by this macro bleed into subsequent terminal output, making it
/// useful for chaining multiple colored segments where the style should carry forward.
///
/// Invalid markup produces a compiler error at the call site.
///
/// # Examples
///
/// ```ignore
/// use farben_macros::colorb;
/// println!("{}", colorb!("[bold red]Bleeds..."));
/// ```
#[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);
    let plain = farben_core::strip::strip_markup(input.value());
    quote! {
        ::farben::FarbenStr { styled: #styled, plain: #plain }
    }
    .into()
}

/// Validates farben markup at compile time and returns the original string literal unchanged.
///
/// On success, emits the input string literal as-is. On failure, emits a compiler error
/// at the call site. Used internally by `color_fmt!`, `cprint!`, and `cprintln!` to validate
/// the format string before runtime processing with format arguments.
///
/// This macro is part of the internal API. Prefer [`color!`] for static strings or
/// `color_fmt!` for strings with format arguments.
#[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()
        }
    }
}

/// Parses and renders an inline markdown string at compile time.
///
/// Tokenizes and renders the input at compile time, emitting the final
/// ANSI-escaped string as a string literal baked into the binary.
/// Supports `**bold**`, `*italic*`, `_italic_`, `__underline__`,
/// `~~strikethrough~~`, and `` `inline code` ``.
///
/// # Examples
///
/// Compiles inline markdown to ANSI-escaped terminal output at compile time.
///
/// Parses markdown syntax (`**bold**`, `*italic*`, `` `code` ``, `~~strike~~`, `__underline__`)
/// and produces terminal-compatible ANSI output.
#[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()
}