nerdle-proc-macro 1.0.0

The macro crate for the nerdle Nerd Font macro library
Documentation
use proc_macro::TokenStream;
use quote::quote;
use syn::{LitStr, parse_macro_input};

include!(concat!(env!("OUT_DIR"), "/icons.rs"));

struct ResolvedGlyph {
    glyph: char,
    codepoint: u32,
}

#[proc_macro]
pub fn nerd(input: TokenStream) -> TokenStream {
    expand_macro(input, MacroKind::Str)
}

#[proc_macro]
pub fn nerd_char(input: TokenStream) -> TokenStream {
    expand_macro(input, MacroKind::Char)
}

#[proc_macro]
pub fn nerd_cp(input: TokenStream) -> TokenStream {
    expand_macro(input, MacroKind::Codepoint)
}

enum MacroKind {
    Str,
    Char,
    Codepoint,
}

fn expand_macro(input: TokenStream, kind: MacroKind) -> TokenStream {
    let input = parse_macro_input!(input as LitStr);

    match resolve_input(&input) {
        Ok(resolved) => match kind {
            MacroKind::Str => {
                let glyph = LitStr::new(&resolved.glyph.to_string(), input.span());
                quote!(#glyph).into()
            }
            MacroKind::Char => {
                let glyph = syn::LitChar::new(resolved.glyph, input.span());
                quote!(#glyph).into()
            }
            MacroKind::Codepoint => {
                let codepoint =
                    syn::LitInt::new(&format!("{}u32", resolved.codepoint), input.span());
                quote!(#codepoint).into()
            }
        },
        Err(error) => error.to_compile_error().into(),
    }
}

fn resolve_input(input: &LitStr) -> syn::Result<ResolvedGlyph> {
    let raw = input.value();

    if let Some(codepoint) = lookup_icon(&raw) {
        return validate_codepoint(input, codepoint)
            .map(|glyph| ResolvedGlyph { glyph, codepoint });
    }

    if let Some(normalized) = normalize_icon_name(&raw)
        && let Some(codepoint) = lookup_icon(&normalized)
    {
        return validate_codepoint(input, codepoint)
            .map(|glyph| ResolvedGlyph { glyph, codepoint });
    }

    let codepoint = parse_codepoint(&raw).ok_or_else(|| {
        syn::Error::new_spanned(
            input,
            format!("unknown Nerd Font glyph or codepoint: {raw}"),
        )
    })?;

    validate_codepoint(input, codepoint).map(|glyph| ResolvedGlyph { glyph, codepoint })
}

fn parse_codepoint(raw: &str) -> Option<u32> {
    let trimmed = raw.trim();
    let hex = if let Some(rest) = trimmed
        .strip_prefix("0x")
        .or_else(|| trimmed.strip_prefix("0X"))
    {
        rest
    } else if let Some(rest) = trimmed
        .strip_prefix("U+")
        .or_else(|| trimmed.strip_prefix("u+"))
    {
        rest
    } else {
        trimmed
    };

    if hex.is_empty() {
        return None;
    }

    u32::from_str_radix(hex, 16).ok()
}

fn normalize_icon_name(raw: &str) -> Option<String> {
    let mut normalized = String::with_capacity(raw.len());
    let mut last_was_separator = false;

    for ch in raw.trim().chars() {
        if matches!(ch, ' ' | '_' | '-') {
            if !normalized.is_empty() && !last_was_separator {
                normalized.push('-');
            }
            last_was_separator = true;
            continue;
        }

        normalized.extend(ch.to_lowercase());
        last_was_separator = false;
    }

    while normalized.ends_with('-') {
        normalized.pop();
    }

    if normalized.is_empty() || normalized == raw {
        None
    } else {
        Some(normalized)
    }
}

fn validate_codepoint(input: &LitStr, codepoint: u32) -> syn::Result<char> {
    char::from_u32(codepoint).ok_or_else(|| {
        syn::Error::new_spanned(
            input,
            format!("invalid Unicode scalar value: 0x{codepoint:X}"),
        )
    })
}