Skip to main content

nerdle_proc_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{LitStr, parse_macro_input};
4
5include!(concat!(env!("OUT_DIR"), "/icons.rs"));
6
7struct ResolvedGlyph {
8    glyph: char,
9    codepoint: u32,
10}
11
12#[proc_macro]
13pub fn nerd(input: TokenStream) -> TokenStream {
14    expand_macro(input, MacroKind::Str)
15}
16
17#[proc_macro]
18pub fn nerd_char(input: TokenStream) -> TokenStream {
19    expand_macro(input, MacroKind::Char)
20}
21
22#[proc_macro]
23pub fn nerd_cp(input: TokenStream) -> TokenStream {
24    expand_macro(input, MacroKind::Codepoint)
25}
26
27enum MacroKind {
28    Str,
29    Char,
30    Codepoint,
31}
32
33fn expand_macro(input: TokenStream, kind: MacroKind) -> TokenStream {
34    let input = parse_macro_input!(input as LitStr);
35
36    match resolve_input(&input) {
37        Ok(resolved) => match kind {
38            MacroKind::Str => {
39                let glyph = LitStr::new(&resolved.glyph.to_string(), input.span());
40                quote!(#glyph).into()
41            }
42            MacroKind::Char => {
43                let glyph = syn::LitChar::new(resolved.glyph, input.span());
44                quote!(#glyph).into()
45            }
46            MacroKind::Codepoint => {
47                let codepoint =
48                    syn::LitInt::new(&format!("{}u32", resolved.codepoint), input.span());
49                quote!(#codepoint).into()
50            }
51        },
52        Err(error) => error.to_compile_error().into(),
53    }
54}
55
56fn resolve_input(input: &LitStr) -> syn::Result<ResolvedGlyph> {
57    let raw = input.value();
58
59    if let Some(codepoint) = lookup_icon(&raw) {
60        return validate_codepoint(input, codepoint)
61            .map(|glyph| ResolvedGlyph { glyph, codepoint });
62    }
63
64    if let Some(normalized) = normalize_icon_name(&raw)
65        && let Some(codepoint) = lookup_icon(&normalized)
66    {
67        return validate_codepoint(input, codepoint)
68            .map(|glyph| ResolvedGlyph { glyph, codepoint });
69    }
70
71    let codepoint = parse_codepoint(&raw).ok_or_else(|| {
72        syn::Error::new_spanned(
73            input,
74            format!("unknown Nerd Font glyph or codepoint: {raw}"),
75        )
76    })?;
77
78    validate_codepoint(input, codepoint).map(|glyph| ResolvedGlyph { glyph, codepoint })
79}
80
81fn parse_codepoint(raw: &str) -> Option<u32> {
82    let trimmed = raw.trim();
83    let hex = if let Some(rest) = trimmed
84        .strip_prefix("0x")
85        .or_else(|| trimmed.strip_prefix("0X"))
86    {
87        rest
88    } else if let Some(rest) = trimmed
89        .strip_prefix("U+")
90        .or_else(|| trimmed.strip_prefix("u+"))
91    {
92        rest
93    } else {
94        trimmed
95    };
96
97    if hex.is_empty() {
98        return None;
99    }
100
101    u32::from_str_radix(hex, 16).ok()
102}
103
104fn normalize_icon_name(raw: &str) -> Option<String> {
105    let mut normalized = String::with_capacity(raw.len());
106    let mut last_was_separator = false;
107
108    for ch in raw.trim().chars() {
109        if matches!(ch, ' ' | '_' | '-') {
110            if !normalized.is_empty() && !last_was_separator {
111                normalized.push('-');
112            }
113            last_was_separator = true;
114            continue;
115        }
116
117        normalized.extend(ch.to_lowercase());
118        last_was_separator = false;
119    }
120
121    while normalized.ends_with('-') {
122        normalized.pop();
123    }
124
125    if normalized.is_empty() || normalized == raw {
126        None
127    } else {
128        Some(normalized)
129    }
130}
131
132fn validate_codepoint(input: &LitStr, codepoint: u32) -> syn::Result<char> {
133    char::from_u32(codepoint).ok_or_else(|| {
134        syn::Error::new_spanned(
135            input,
136            format!("invalid Unicode scalar value: 0x{codepoint:X}"),
137        )
138    })
139}