use super::inference::infer_control_from_values;
use super::{Palette, PaletteOptions, PaletteOutput, PropControl};
use crate::types::ArtDescriptor;
use vize_carton::FxHashMap;
#[inline]
pub fn generate_palette(art: &ArtDescriptor<'_>, options: &PaletteOptions) -> PaletteOutput {
let mut palette = Palette::new(art.metadata.title);
let mut all_values: FxHashMap<String, Vec<serde_json::Value>> = FxHashMap::default();
for variant in &art.variants {
for (key, value) in &variant.args {
all_values
.entry(key.to_string())
.or_default()
.push(value.clone());
}
}
palette.all_values = all_values.clone();
let mut prop_names: Vec<_> = all_values.keys().collect();
prop_names.sort();
for prop_name in prop_names {
let values = &all_values[prop_name];
let (control_kind, select_options, range_config) =
infer_control_from_values(values, options);
let mut control = PropControl {
name: prop_name.clone(),
control: control_kind,
default_value: values.first().cloned(),
description: None,
required: false,
options: select_options,
range: range_config,
group: if options.group_by_type {
Some(format!("{:?}", control_kind))
} else {
None
},
};
if options.group_by_type {
control.group = Some(format!("{:?}", control.control));
}
palette.add_control(control);
}
let json = serde_json::to_string_pretty(&palette).unwrap_or_default();
let typescript = generate_typescript_interface(&palette);
PaletteOutput {
palette,
json,
typescript,
}
}
fn generate_typescript_interface(palette: &Palette) -> String {
let mut ts = String::with_capacity(512);
ts.push_str("export interface ");
ts.push_str(&to_pascal_case(&palette.title));
ts.push_str("Props {\n");
for control in &palette.controls {
ts.push_str(" ");
ts.push_str(&control.name);
if !control.required {
ts.push('?');
}
ts.push_str(": ");
ts.push_str(&control_to_ts_type(control));
ts.push_str(";\n");
}
ts.push_str("}\n");
ts
}
fn control_to_ts_type(control: &PropControl) -> String {
use super::ControlKind;
match control.control {
ControlKind::Text => "string".to_string(),
ControlKind::Number | ControlKind::Range => "number".to_string(),
ControlKind::Boolean => "boolean".to_string(),
ControlKind::Color => "string".to_string(),
ControlKind::Date => "string | Date".to_string(),
ControlKind::Select | ControlKind::Radio => {
if control.options.is_empty() {
"string".to_string()
} else {
control
.options
.iter()
.map(|opt| match &opt.value {
serde_json::Value::String(s) => format!("'{}'", s),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => "unknown".to_string(),
})
.collect::<Vec<_>>()
.join(" | ")
}
}
ControlKind::Array => "unknown[]".to_string(),
ControlKind::Object => "Record<string, unknown>".to_string(),
ControlKind::File => "File".to_string(),
ControlKind::Raw => "unknown".to_string(),
}
}
#[inline]
fn to_pascal_case(s: &str) -> String {
s.split(|c: char| !c.is_alphanumeric())
.filter(|s| !s.is_empty())
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
None => String::new(),
}
})
.collect()
}
#[allow(dead_code)]
pub fn generate_vue_props(palette: &Palette) -> String {
let mut vue = String::with_capacity(512);
vue.push_str("const props = defineProps<{\n");
for control in &palette.controls {
vue.push_str(" ");
vue.push_str(&control.name);
if !control.required {
vue.push('?');
}
vue.push_str(": ");
vue.push_str(&control_to_ts_type(control));
vue.push('\n');
}
vue.push_str("}>()\n");
vue
}
#[allow(dead_code)]
pub fn generate_storybook_argtypes(palette: &Palette) -> String {
use super::ControlKind;
let mut sb = String::with_capacity(1024);
sb.push_str("export const argTypes = {\n");
for control in &palette.controls {
sb.push_str(" ");
sb.push_str(&control.name);
sb.push_str(": {\n");
sb.push_str(" control: { type: '");
sb.push_str(match control.control {
ControlKind::Text => "text",
ControlKind::Number => "number",
ControlKind::Boolean => "boolean",
ControlKind::Range => "range",
ControlKind::Select => "select",
ControlKind::Radio => "radio",
ControlKind::Color => "color",
ControlKind::Date => "date",
ControlKind::Object => "object",
ControlKind::Array => "object",
ControlKind::File => "file",
ControlKind::Raw => "text",
});
sb.push('\'');
if let Some(ref range) = control.range {
sb.push_str(&format!(", min: {}, max: {}", range.min, range.max));
if let Some(step) = range.step {
sb.push_str(&format!(", step: {}", step));
}
}
sb.push_str(" },\n");
if !control.options.is_empty() {
sb.push_str(" options: [");
for (i, opt) in control.options.iter().enumerate() {
if i > 0 {
sb.push_str(", ");
}
match &opt.value {
serde_json::Value::String(s) => sb.push_str(&format!("'{}'", s)),
serde_json::Value::Number(n) => sb.push_str(&n.to_string()),
serde_json::Value::Bool(b) => sb.push_str(&b.to_string()),
_ => sb.push_str("null"),
}
}
sb.push_str("],\n");
}
if let Some(ref desc) = control.description {
sb.push_str(&format!(" description: '{}',\n", desc));
}
if let Some(ref default) = control.default_value {
sb.push_str(" defaultValue: ");
match default {
serde_json::Value::String(s) => sb.push_str(&format!("'{}'", s)),
serde_json::Value::Number(n) => sb.push_str(&n.to_string()),
serde_json::Value::Bool(b) => sb.push_str(&b.to_string()),
_ => sb.push_str(&default.to_string()),
}
sb.push_str(",\n");
}
if let Some(ref group) = control.group {
sb.push_str(&format!(" table: {{ category: '{}' }},\n", group));
}
sb.push_str(" },\n");
}
sb.push_str("};\n");
sb
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_art, ArtParseOptions, Bump};
#[test]
fn test_generate_palette_basic() {
let allocator = Bump::new();
let source = r#"
<art title="Button">
<variant name="Primary" args='{"variant":"primary","size":"md","disabled":false}'>
<Button>Click</Button>
</variant>
<variant name="Secondary" args='{"variant":"secondary","size":"lg","disabled":true}'>
<Button>Click</Button>
</variant>
</art>
"#;
let art = parse_art(&allocator, source, ArtParseOptions::default()).unwrap();
let output = generate_palette(&art, &PaletteOptions::default());
assert_eq!(output.palette.title, "Button");
assert_eq!(output.palette.controls.len(), 3);
let variant_ctrl = output
.palette
.controls
.iter()
.find(|c| c.name == "variant")
.unwrap();
assert_eq!(variant_ctrl.control, super::super::ControlKind::Select);
assert_eq!(variant_ctrl.options.len(), 2);
let disabled_ctrl = output
.palette
.controls
.iter()
.find(|c| c.name == "disabled")
.unwrap();
assert_eq!(disabled_ctrl.control, super::super::ControlKind::Boolean);
}
#[test]
fn test_generate_typescript_interface() {
let mut palette = Palette::new("Button");
palette.add_control(PropControl::text("label").required());
palette.add_control(PropControl::boolean("disabled"));
palette.add_control(PropControl::number("size"));
let ts = generate_typescript_interface(&palette);
assert!(ts.contains("export interface ButtonProps"));
assert!(ts.contains("label: string;"));
assert!(ts.contains("disabled?: boolean;"));
assert!(ts.contains("size?: number;"));
}
#[test]
fn test_generate_storybook_argtypes() {
let mut palette = Palette::new("Button");
palette.add_control(
PropControl::select(
"size",
vec![
super::super::SelectOption {
label: "Small".to_string(),
value: serde_json::json!("sm"),
},
super::super::SelectOption {
label: "Large".to_string(),
value: serde_json::json!("lg"),
},
],
)
.with_default(serde_json::json!("sm")),
);
let argtypes = generate_storybook_argtypes(&palette);
assert!(argtypes.contains("control: { type: 'select' }"));
assert!(argtypes.contains("options: ['sm', 'lg']"));
assert!(argtypes.contains("defaultValue: 'sm'"));
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("button"), "Button");
assert_eq!(to_pascal_case("my-button"), "MyButton");
assert_eq!(to_pascal_case("my_button"), "MyButton");
assert_eq!(to_pascal_case("MyButton"), "MyButton");
}
}