Skip to main content

aphyn_math_macros/
lib.rs

1//! # aphyn_math_macros
2//!
3//! Compile-time hex color literals: turn a `"#rrggbb"` / `"#rrggbbaa"` string
4//! into a fixed-size color array so a wrong color is a build error, not a
5//! runtime surprise.
6//!
7//! # How to use
8//!
9//! Each macro takes a single string literal and expands, at compile time, to a
10//! plain array literal — so the result is a `const`-usable value with no runtime
11//! parsing cost. Pick the macro that matches the channel count (RGB vs RGBA) and
12//! component type (`u8` 0–255 or `f32` 0.0–1.0) the call site needs. The hash
13//! prefix is optional; a malformed literal aborts compilation with a clear
14//! message.
15//!
16//! #### Build an 8-bit RGBA color
17//!
18//! Parse a `"#rrggbbaa"` literal into `[u8; 4]` for an API that expects four
19//! byte channels.
20//!
21//! ```
22//! let color = aphyn_math_macros::rgba_u8_array!("#ff8800ff");
23//! assert_eq!(color, [255, 136, 0, 255]);
24//! ```
25//!
26//! #### Build a normalized RGB color
27//!
28//! Parse a `"#rrggbb"` literal into `[f32; 3]`, each channel scaled to the
29//! `0.0..=1.0` range a shader or GPU clear color expects.
30//!
31//! ```
32//! let color = aphyn_math_macros::rgb_f32_array!("#ff0000");
33//! assert_eq!(color, [1.0, 0.0, 0.0]);
34//! ```
35//!
36//! #### The other channel layouts
37//!
38//! [`rgb_u8_array!`](crate::rgb_u8_array) yields `[u8; 3]` and
39//! [`rgba_f32_array!`](crate::rgba_f32_array) yields `[f32; 4]`; both follow the
40//! same literal format as the examples above.
41//!
42//! ```
43//! assert_eq!(aphyn_math_macros::rgb_u8_array!("#ff8800"), [255, 136, 0]);
44//! assert_eq!(aphyn_math_macros::rgba_f32_array!("#ff0000ff"), [1.0, 0.0, 0.0, 1.0]);
45//! ```
46//!
47//! # How to maintain
48//!
49//! The crate is a thin proc-macro front end over [`aphyn_math`]'s `const`
50//! color parsers: each macro extracts the inner text of the input string literal
51//! and forwards it to the matching `aphyn_math::parse_color_*` function, then
52//! prints the returned array back out as Rust source that re-parses into a
53//! `TokenStream`. All real parsing — hex decoding, channel ordering, and the
54//! `u8 / 255.0` normalization for the `f32` variants — lives in `aphyn_math`;
55//! this crate only moves tokens.
56//!
57//! Two helpers do the token bridging. `to_string_literal` takes the first
58//! `TokenTree`, requires it to be a `Literal`, and `to_exact_string_literal`
59//! strips the surrounding quotes by hand — it checks the first and last
60//! characters are `"` and copies the rest verbatim. It does **not** interpret
61//! escape sequences, so the macros assume a simple `"#..."` literal; raw or
62//! escaped string literals are not supported. `to_token_stream` renders the
63//! parsed array with `{:?}` and re-parses the text, which is why the input must
64//! be a single literal token and not, say, a `const` expression.
65//!
66//! Every failure path — a missing token, a non-literal token, a value
67//! `aphyn_math` rejects, or a re-parse error — funnels through
68//! `panic_invalid_format`, so any misuse surfaces as the same compile-time
69//! panic naming the two accepted formats.
70
71use std::fmt::Debug;
72
73use proc_macro::{Literal, TokenStream, TokenTree};
74
75#[proc_macro]
76pub fn rgba_u8_array(stream: TokenStream) -> TokenStream {
77    to_token_stream(aphyn_math::parse_color_rgba_u8(&to_string_literal(stream)).unwrap_or_else(|| panic_invalid_format()))
78}
79
80#[proc_macro]
81pub fn rgb_u8_array(stream: TokenStream) -> TokenStream {
82    to_token_stream(aphyn_math::parse_color_rgb_u8(&to_string_literal(stream)).unwrap_or_else(|| panic_invalid_format()))
83}
84
85#[proc_macro]
86pub fn rgba_f32_array(stream: TokenStream) -> TokenStream {
87    to_token_stream(aphyn_math::parse_color_rgba_f32(&to_string_literal(stream)).unwrap_or_else(|| panic_invalid_format()))
88}
89
90#[proc_macro]
91pub fn rgb_f32_array(stream: TokenStream) -> TokenStream {
92    to_token_stream(aphyn_math::parse_color_rgb_f32(&to_string_literal(stream)).unwrap_or_else(|| panic_invalid_format()))
93}
94
95fn to_string_literal(stream: TokenStream) -> String {
96    let mut iter = stream.into_iter();
97
98    let literal = iter.next().unwrap_or_else(|| panic_invalid_format());
99
100    let TokenTree::Literal(literal) = literal else {
101        panic_invalid_format();
102    };
103
104    to_exact_string_literal(literal)
105}
106
107fn to_token_stream<T: Debug, const N: usize>(v: [T; N]) -> TokenStream {
108    let mut result = String::new();
109
110    result.push_str("[");
111    result.push_str(&v.into_iter().map(|x| format!("{:?}", x)).collect::<Vec<_>>().join(", "));
112    result.push_str("]");
113
114    result.parse().unwrap_or_else(|_| panic_invalid_format())
115}
116
117fn to_exact_string_literal(lit: Literal) -> String {
118    let lit = lit.to_string();
119
120    let mut chars = lit.chars();
121
122    let Some('"') = chars.next() else {
123        panic_invalid_format();
124    };
125
126    let mut result = String::new();
127
128    let mut chars = chars.peekable();
129
130    if chars.peek().is_none() {
131        panic_invalid_format();
132    }
133
134    loop {
135        let c = chars.next().unwrap();
136
137        if chars.peek().is_some() {
138            result.push(c);
139            continue;
140        }
141
142        if c != '"' {
143            panic_invalid_format();
144        }
145
146        break;
147    }
148
149    result
150}
151
152fn panic_invalid_format() -> ! {
153    panic!("macro expects a single string literal of format \"#ffffffff\" or \"#ffffff\"");
154}