1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, DeriveInput, Data, Fields};
4
5fn get_dominator_crate_attr(input: &DeriveInput) -> String {
8 for attr in &input.attrs {
9 if attr.path().is_ident("dominator_crate") {
10 if let Ok(syn::Expr::Lit(syn::ExprLit {
11 lit: syn::Lit::Str(lit_str),
12 ..
13 })) = attr.parse_args::<syn::Expr>()
14 {
15 return lit_str.value();
16 }
17 }
18 }
19 "dominator".to_string()
20}
21
22fn get_story_attrs(field: &syn::Field) -> (Option<String>, Option<String>, Option<syn::Type>, Option<usize>, bool) {
25 let mut control_type = None;
26 let mut default_value = None;
27 let mut from_type = None;
28 let mut lorem_count = None;
29 let mut skip = false;
30
31 for attr in &field.attrs {
32 if attr.path().is_ident("story") {
33 let _ = attr.parse_nested_meta(|meta| {
35 if meta.path.is_ident("control") {
36 if let Ok(value) = meta.value() {
37 if let Ok(lit_str) = value.parse::<syn::LitStr>() {
38 control_type = Some(lit_str.value());
39 }
40 }
41 } else if meta.path.is_ident("default") {
42 if let Ok(value) = meta.value() {
43 if let Ok(lit_str) = value.parse::<syn::LitStr>() {
44 default_value = Some(lit_str.value());
45 }
46 }
47 } else if meta.path.is_ident("from") {
48 if let Ok(value) = meta.value() {
49 if let Ok(lit_str) = value.parse::<syn::LitStr>() {
50 from_type =
51 Some(syn::parse_str(&lit_str.value()).expect("Invalid type for from"));
52 }
53 }
54 } else if meta.path.is_ident("lorem") {
55 if let Ok(value) = meta.value() {
57 if let Ok(lit_str) = value.parse::<syn::LitStr>() {
58 if let Ok(count) = lit_str.value().parse::<usize>() {
59 lorem_count = Some(count);
60 }
61 }
62 } else {
63 lorem_count = Some(8);
65 }
66 } else if meta.path.is_ident("skip") {
67 skip = true;
68 }
69 Ok(())
70 });
71 }
72 }
73
74 (control_type, default_value, from_type, lorem_count, skip)
75}
76
77fn generate_lorem_ipsum(word_count: usize) -> String {
79 const LOREM_WORDS: &[&str] = &[
80 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
81 "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et",
82 "dolore", "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis",
83 "nostrud", "exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea",
84 "commodo", "consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
85 "velit", "esse", "cillum", "fugiat", "nulla", "pariatur", "excepteur", "sint",
86 "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia",
87 "deserunt", "mollit", "anim", "id", "est", "laborum", "pellentesque", "habitant",
88 "morbi", "tristique", "senectus", "netus", "et", "malesuada", "fames", "ac",
89 "turpis", "egestas", "vestibulum", "tortor", "quam", "feugiat", "vitae", "ultricies",
90 "legimus", "typi", "qui", "nusquam", "vici", "sunt", "signa", "consuetudium"
91 ];
92
93 let mut words = Vec::new();
94 for i in 0..word_count {
95 words.push(LOREM_WORDS[i % LOREM_WORDS.len()]);
96 }
97 words.join(" ")
98}
99
100
101fn generate_storybook_js(name: &str, _fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, arg_types: &[(String, String, String, String, String)]) {
102 let arg_types_json: Vec<String> = arg_types.iter().map(|(field_name, control, _default_val, required, options_json)| {
104 let options_str = if !options_json.is_empty() {
105 format!(", options: {}", options_json)
106 } else {
107 String::new()
108 };
109
110 let required_str = if required == "true" {
111 ", table: { category: 'required' }"
112 } else {
113 ""
114 };
115
116 format!(
117 " {}: {{\n control: '{}',\n description: '{}'{}{}\n }}",
118 field_name, control, field_name, options_str, required_str
119 )
120 }).collect();
121
122 let args_str = arg_types_json.join(",\n");
123
124 let default_args: Vec<String> = arg_types.iter().map(|(field_name, _, default_val, _, _)| {
126 format!(" {}: {}", field_name, default_val)
127 }).collect();
128
129 let default_args_str = default_args.join(",\n");
130
131 let js_content = format!(r#"import init, {{ register_all_stories, render_story, get_enum_options, init_enums }} from '../../example/pkg/example.js';
132
133// Initialize WASM
134await init();
135
136console.log('About to call init_enums...');
137init_enums();
138console.log('init_enums called');
139
140register_all_stories();
141
142// Define the story with populated enum options
143export default {{
144 title: 'Components/{}',
145 argTypes: {{
146{}
147 }},
148}};
149
150const Template = (args) => {{
151 const container = document.createElement('div');
152 const dom = render_story('{}', args);
153 container.appendChild(dom);
154 return container;
155}};
156
157export const Default = Template.bind({{}});
158Default.args = {{
159{}
160}};
161"#, name, args_str, name, default_args_str);
162
163 let output_dir = std::env::var("CARGO_MANIFEST_DIR")
165 .map(|d| std::path::PathBuf::from(d).parent().unwrap().join("storybook/stories"))
166 .unwrap_or_else(|_| std::path::PathBuf::from("storybook/stories"));
167
168 if let Err(_) = std::fs::create_dir_all(&output_dir) {
169 }
171
172 let output_file = output_dir.join(format!("{}.stories.js", name));
173 let _ = std::fs::write(output_file, js_content);
174}
175
176#[proc_macro_attribute]
190pub fn set_dominator_path(_args: TokenStream, input: TokenStream) -> TokenStream {
191 input
194}
195
196#[proc_macro_derive(Story, attributes(story, dominator_crate))]
197pub fn derive_story(input: TokenStream) -> TokenStream {
198 let input = parse_macro_input!(input as DeriveInput);
199 let _dominator_crate = get_dominator_crate_attr(&input);
200 let name = &input.ident;
201 let generics = &input.generics;
202 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
203 let name_str = name.to_string();
204 let story_args_name = syn::Ident::new(&format!("{}StoryArgs", name), name.span());
205
206 let fields = match &input.data {
208 Data::Struct(data) => match &data.fields {
209 Fields::Named(fields) => &fields.named,
210 _ => panic!("Story can only be derived for structs with named fields"),
211 },
212 _ => panic!("Story can only be derived for structs"),
213 };
214
215 let story_args_fields = fields.iter().filter_map(|field| {
216 let field_name = &field.ident;
217 let field_ty = &field.ty;
218 let (control_type, _, from_type, _, skip) = get_story_attrs(field);
219
220 if skip {
222 return None;
223 }
224
225 let should_be_optional = control_type.as_ref().map(|c| c == "select").unwrap_or(false);
227
228 let field_def = if let Some(from_type) = from_type {
229 if should_be_optional {
230 quote! {
231 #[serde(default)]
232 pub #field_name: Option<#from_type>
233 }
234 } else {
235 quote! {
236 #[serde(default)]
237 pub #field_name: #from_type
238 }
239 }
240 } else {
241 if should_be_optional {
242 quote! {
243 #[serde(default)]
244 pub #field_name: Option<#field_ty>
245 }
246 } else {
247 quote! {
248 #[serde(default)]
249 pub #field_name: #field_ty
250 }
251 }
252 };
253
254 Some(field_def)
255 });
256
257 let from_impl_fields = fields.iter().map(|field| {
258 let field_name = &field.ident;
259 let (control_type, _, _, _, skip) = get_story_attrs(field);
260
261 if skip {
262 return quote! { #field_name: Default::default() };
264 }
265
266 let should_be_optional = control_type.as_ref().map(|c| c == "select").unwrap_or(false);
267
268 if should_be_optional {
269 quote! { #field_name: value.#field_name.unwrap_or_default() }
271 } else {
272 quote! { #field_name: value.#field_name.into() }
273 }
274 });
275
276 let mut arg_types_for_js: Vec<(String, String, String, String, String)> = Vec::new();
278 let mut arg_types_vec = Vec::new();
279
280 for field in fields.iter() {
281 let field_name = &field.ident;
282 let field_name_str = field_name.as_ref().unwrap().to_string();
283 let field_ty = &field.ty;
284 let ty_string = quote!(#field_ty).to_string();
285 let is_option = ty_string.starts_with("Option <");
286
287 let (control_type, default_value, from_type, lorem_count, skip) = get_story_attrs(field);
288
289 if skip {
291 continue;
292 }
293
294 let mut options = quote! { None };
295 let mut options_json = String::new();
296 let control = if let Some(ref control_type) = control_type {
297 match control_type.as_str() {
298 "color" => quote! { storybook::ControlType::Color },
299 "select" => {
300 options = quote! { Some(<#field_ty as storybook::StorySelect>::options()) };
301 let enum_type_name = ty_string.trim().replace(" ", "");
303 options_json = format!("get_enum_options('{}')", enum_type_name);
304 quote! { storybook::ControlType::Select }
305 }
306 _ => quote! { storybook::ControlType::Text },
307 }
308 } else {
309 let ty_to_check = if let Some(from_type) = &from_type {
310 quote!(#from_type).to_string()
311 } else {
312 ty_string.clone()
313 };
314
315 if ty_to_check.contains("bool") {
316 quote! { storybook::ControlType::Boolean }
317 } else if ty_to_check.contains("i32")
318 || ty_to_check.contains("f32")
319 || ty_to_check.contains("u32")
320 || ty_to_check.contains("f64")
321 || ty_to_check.contains("usize")
322 {
323 quote! { storybook::ControlType::Number }
324 } else {
325 quote! { storybook::ControlType::Text }
326 }
327 };
328
329 let default_value_quoted = match &default_value {
330 Some(v) => quote! { Some(#v.to_string()) },
331 None => {
332 if let Some(lorem_word_count) = lorem_count {
333 let lorem_text = generate_lorem_ipsum(lorem_word_count);
334 quote! { Some(#lorem_text.to_string()) }
335 } else {
336 quote! { None }
337 }
338 }
339 };
340
341 let control_str = match control_type.as_ref() {
342 Some(ct) => {
343 match ct.as_str() {
344 "color" => "color".to_string(),
345 "select" => "select".to_string(),
346 _ => "text".to_string(),
347 }
348 }
349 None => {
350 if ty_string.contains("bool") {
351 "boolean".to_string()
352 } else if ty_string.contains("i32") || ty_string.contains("f32") || ty_string.contains("u32") || ty_string.contains("f64") || ty_string.contains("usize") {
353 "number".to_string()
354 } else {
355 "text".to_string()
356 }
357 }
358 };
359
360 let default_val_str = match &default_value {
361 Some(dv) => dv.clone(),
362 None => {
363 if let Some(lorem_word_count) = lorem_count {
364 format!("'{}'", generate_lorem_ipsum(lorem_word_count))
366 } else if control_str == "select" {
367 "null".to_string()
368 } else if ty_string.contains("String") {
369 "''".to_string()
370 } else if ty_string.contains("bool") {
371 "false".to_string()
372 } else if ty_string.contains("i32") || ty_string.contains("f32") || ty_string.contains("u32") || ty_string.contains("f64") || ty_string.contains("usize") {
373 "0".to_string()
374 } else {
375 "undefined".to_string()
376 }
377 }
378 };
379
380 arg_types_for_js.push((
381 field_name_str.clone(),
382 control_str,
383 default_val_str,
384 if is_option { "false" } else { "true" }.to_string(),
385 options_json,
386 ));
387
388 arg_types_vec.push(quote! {
389 storybook::ArgType {
390 name: #field_name_str.to_string(),
391 default_value: #default_value_quoted,
392 control: #control,
393 required: !#is_option,
394 options: #options,
395 }
396 });
397 }
398
399 generate_storybook_js(&name_str, fields, &arg_types_for_js);
401
402 let expanded = quote! {
404 #[derive(serde::Deserialize, Default)]
405 pub struct #story_args_name {
406 #(#story_args_fields),*
407 }
408
409 impl From<#story_args_name> for #name {
410 fn from(value: #story_args_name) -> Self {
411 Self {
412 #(#from_impl_fields),*
413 }
414 }
415 }
416
417 impl #impl_generics storybook::StoryMeta for #name #ty_generics #where_clause {
418 type StoryArgs = #story_args_name;
419
420 fn name() -> &'static str {
421 #name_str
422 }
423
424 fn args() -> Vec<storybook::ArgType> {
425 vec![
426 #(#arg_types_vec),*
427 ]
428 }
429 }
430 };
431
432 TokenStream::from(expanded)
433}
434
435#[proc_macro_derive(StorySelect, attributes(story_select))]
441pub fn derive_story_select(input: TokenStream) -> TokenStream {
442 let input = parse_macro_input!(input as DeriveInput);
443 let name = &input.ident;
444 let generics = &input.generics;
445 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
446
447 let variants = match &input.data {
449 Data::Enum(data) => &data.variants,
450 _ => panic!("StorySelect can only be derived for enums"),
451 };
452
453 let options = variants.iter().map(|variant| {
455 let variant_name = &variant.ident;
456 let variant_str = variant_name.to_string();
457
458 quote! {
459 #variant_str.to_string()
460 }
461 });
462
463 let from_str_arms = variants.iter().map(|variant| {
465 let variant_name = &variant.ident;
466 let variant_str = variant_name.to_string();
467
468 quote! {
469 #variant_str => Ok(#name::#variant_name)
470 }
471 });
472
473 let display_arms = variants.iter().map(|variant| {
475 let variant_name = &variant.ident;
476 let variant_str = variant_name.to_string();
477
478 quote! {
479 #name::#variant_name => #variant_str
480 }
481 });
482
483 let name_str = name.to_string();
484
485 let expanded = quote! {
487 impl #impl_generics storybook::StorySelect for #name #ty_generics #where_clause {
488 fn type_name() -> &'static str {
489 #name_str
490 }
491
492 fn options() -> Vec<String> {
493 vec![
494 #(#options),*
495 ]
496 }
497 }
498
499 impl #impl_generics #name #ty_generics #where_clause {
501 #[doc(hidden)]
502 pub fn __register_enum_options() {
503 storybook::register_enum_options(
504 #name_str,
505 <#name as storybook::StorySelect>::options()
506 );
507 }
508 }
509
510 impl #impl_generics std::str::FromStr for #name #ty_generics #where_clause {
511 type Err = String;
512
513 fn from_str(s: &str) -> Result<Self, Self::Err> {
514 match s {
515 #(#from_str_arms,)*
516 _ => Err(format!("Invalid {} variant: {}", #name_str, s))
517 }
518 }
519 }
520
521 impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 let s = match self {
524 #(#display_arms,)*
525 };
526 write!(f, "{}", s)
527 }
528 }
529 };
530
531 TokenStream::from(expanded)
532}
533
534#[proc_macro]
537pub fn register_stories(input: TokenStream) -> TokenStream {
538 let types = syn::parse_macro_input!(input with syn::punctuated::Punctuated::<syn::Type, syn::Token![,]>::parse_terminated);
539
540 let registrations = types.iter().map(|ty| {
541 quote! {
542 storybook::register_story::<#ty>();
543 }
544 });
545
546 let expanded = quote! {
547 #[wasm_bindgen::prelude::wasm_bindgen]
548 pub fn register_all_stories() {
549 #(#registrations)*
550 }
551 };
552
553 TokenStream::from(expanded)
554}
555
556#[proc_macro]
559pub fn register_enums(input: TokenStream) -> TokenStream {
560 let types = syn::parse_macro_input!(input with syn::punctuated::Punctuated::<syn::Type, syn::Token![,]>::parse_terminated);
561
562 let registrations = types.iter().map(|ty| {
563 quote! {
564 #ty::__register_enum_options();
565 }
566 });
567
568 let expanded = quote! {
569 #[wasm_bindgen::prelude::wasm_bindgen]
570 pub fn init_enums() {
571 #(#registrations)*
572 }
573 };
574
575 TokenStream::from(expanded)
576}