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}