1#![feature(proc_macro_expand)]
2
3use proc_macro::TokenStream;
4use quote::quote;
5use serde_json::Value;
6use syn::{LitStr, parse_macro_input};
7
8enum ParsedColor {
9 RGB(u8, u8, u8),
10 RGBA(u8, u8, u8, u8),
11 P3(f32, f32, f32),
12}
13
14#[proc_macro]
15pub fn generate_color_constants(input: TokenStream) -> TokenStream {
16 let input = input.expand_expr().expect("Failed to expand input");
17
18 let str_literal = parse_macro_input!(input as LitStr).value();
19
20 let colors: Value =
22 serde_json::from_str(&str_literal).expect("Failed to parse JSON content");
23
24 let mut color_tokens = quote! {};
26
27 fn hex_to_rgb(hex: &str) -> Option<ParsedColor> {
29 if hex.len() == 9 && hex.starts_with('#') {
30 let r = u8::from_str_radix(&hex[1..3], 16).unwrap_or(0);
32 let g = u8::from_str_radix(&hex[3..5], 16).unwrap_or(0);
33 let b = u8::from_str_radix(&hex[5..7], 16).unwrap_or(0);
34 let a = u8::from_str_radix(&hex[7..9], 16).unwrap_or(0);
35 Some(ParsedColor::RGBA(r, g, b, a))
36 } else if hex.len() == 7 && hex.starts_with('#') {
37 let r = u8::from_str_radix(&hex[1..3], 16).unwrap_or(0);
39 let g = u8::from_str_radix(&hex[3..5], 16).unwrap_or(0);
40 let b = u8::from_str_radix(&hex[5..7], 16).unwrap_or(0);
41 Some(ParsedColor::RGB(r, g, b))
42 }
43 else if hex.starts_with("color(display-p3") {
45 let parts: Vec<&str> = hex
46 .trim_start_matches("color(display-p3")
47 .trim_end_matches(")")
48 .split_whitespace()
49 .collect();
50 if parts.len() == 3 {
51 let r = parts[0].parse::<f32>().unwrap();
52 let g = parts[1].parse::<f32>().unwrap();
53 let b = parts[2].parse::<f32>().unwrap();
54 Some(ParsedColor::P3(r, g, b))
55 } else {
56 None
57 }
58 } else {
59 None
60 }
61 }
62
63 if let Value::Object(palettes) = colors {
65 for (palette_name, palette_values) in palettes {
66 if let Value::Object(colors) = palette_values {
67 for (color_name, color_value) in colors {
69 if let Value::String(hex) = color_value {
70 let Some(color) = hex_to_rgb(&hex) else {
71 continue;
72 };
73
74 let color_ident = syn::Ident::new(
75 &format!(
76 "{}_{}",
77 palette_name.to_uppercase(),
78 color_name.to_uppercase()
79 ),
80 proc_macro2::Span::call_site(),
81 );
82
83 match color {
84 ParsedColor::RGB(r, g, b) => {
85 color_tokens.extend(quote! {
86 pub const #color_ident: ColorU8 = ColorU8 { r: #r, g: #g, b: #b };
87 });
88 }
89 ParsedColor::RGBA(r, g, b, a) => {
90 color_tokens.extend(quote! {
91 pub const #color_ident: ColorU8A = ColorU8A { r: #r, g: #g, b: #b, a: #a };
92 });
93 }
94 ParsedColor::P3(r, g, b) => {
95 color_tokens.extend(quote! {
96 pub const #color_ident: ColorP3 = ColorP3 { r: #r, g: #g, b: #b };
97 });
98 }
99 }
100 }
101 }
102 }
103 }
104 }
105
106 let output = quote! {
108 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
109 pub struct ColorU8 {
110 pub r: u8,
111 pub g: u8,
112 pub b: u8,
113 }
114
115 impl ColorU8 {
116 pub fn hex(&self) -> String {
117 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
118 }
119 pub fn u8(&self) -> (u8, u8, u8) {
120 (self.r, self.g, self.b)
121 }
122 }
123
124 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
125 pub struct ColorU8A {
126 pub r: u8,
127 pub g: u8,
128 pub b: u8,
129 pub a: u8,
130 }
131
132 impl ColorU8A {
133 pub fn hex(&self) -> String {
134 format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
135 }
136 pub fn u8(&self) -> (u8, u8, u8, u8) {
137 (self.r, self.g, self.b, self.a)
138 }
139 }
140
141 #[derive(Debug, Clone, Copy, PartialEq)]
142 pub struct ColorP3 {
143 pub r: f32,
144 pub g: f32,
145 pub b: f32,
146 }
147
148 impl ColorP3 {
149 pub fn html(&self) -> String {
150 format!("color(display-p3 {} {} {})", self.r, self.g, self.b)
151 }
152 pub fn f32(&self) -> (f32, f32, f32) {
153 (self.r, self.g, self.b)
154 }
155 }
156
157 #color_tokens
158 };
159
160 output.into()
161}