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}"),
)
})
}