//! Component code: a component (or component set) becomes a React, Leptos, Vue, SwiftUI, Jetpack //! Compose or ArkUI component whose properties are its parameters (a variant property → a union or //! enum, boolean → a flag showing the layers bound to it, text → a string, instance swap → a view //! parameter, slot → optional content), and an instance becomes a call passing the values it sets. //! Variants of a set share one definition (the set's default variant); in React and Leptos their //! differences become CSS rules on `data-*` attributes. Returned by `inspect.componentCode` and //! used by `inspect.code`. A component connected to code (see [`crate::code_connect`]) shows its //! real snippet instead. use figcraft_doc::*; use serde_json::{Value, json}; use crate::cmd::CommandSpec; use crate::codegen::{CodeLang, Ctx, Markup, arkui, class_name, compose, indent, literal, swift}; use crate::components::{prop_default_value, slot_edited, slots_of, variant_options, variant_props, variant_set}; use crate::params::*; use crate::{bad, spec}; /// What a component property becomes in code. #[derive(Clone, Debug, PartialEq)] pub enum ParamKind { Boolean, Text, InstanceSwap, /// A variant property with its values. Variant(Vec), Slot, } impl ParamKind { pub fn id(&self) -> &'static str { match self { ParamKind::Boolean => "boolean", ParamKind::Text => "text", ParamKind::InstanceSwap => "instanceSwap", ParamKind::Variant(_) => "variant", ParamKind::Slot => "slot", } } } /// A component property as a parameter. #[derive(Clone, Debug, PartialEq)] pub struct Param { /// The property's name. pub prop: String, /// Its name in code: camelCase, unique, never a keyword. pub ident: String, pub kind: ParamKind, /// The default: a bool, a string, a component id (instance swap) or null (slot). pub default: Value, /// Variant properties: the enum's name in SwiftUI (PascalCase, unique, never a type the /// generated code uses); Compose prefixes it with the component's name. Empty otherwise. pub enum_name: String, } /// A component as code: the main whose layers are drawn (a set's default variant), its set, its /// name in code and its parameters (variant properties first). #[derive(Clone, Debug, PartialEq)] pub struct Component { pub comp: NodeId, pub set: Option, pub type_name: String, pub params: Vec, } pub fn specs() -> Vec { vec![spec!( "inspect.componentCode", "Component code", "{id? (component, set or instance; default the selection), language?: react|leptos|vue|swiftui|compose|arkui (default react)} → {component, name, set, properties: [{name, param, type, default, value? (instances)}], definition, usage? (instances; the connected snippet when the component is connected to code), connected: {importPath, name, template, import, code} | null}", false, |s, p| { let cmd = "inspect.componentCode"; let id = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select a component or instance"))?; let lang = match string(p, "language") { // One list of code languages, shared with codeConnect (see `code_connect::language`). Some(l) => crate::code_connect::language(&l).ok_or_else(|| bad(cmd, "language: react|leptos|vue|swiftui|compose|arkui"))?, None => CodeLang::React, }; let doc = s.doc()?; let c = main_of(doc, id).and_then(|m| describe(doc, m)).ok_or_else(|| bad(cmd, "not a component, set or instance"))?; let values = instance_values(doc, id, &c); let properties: Vec = c .params .iter() .map(|p| { let mut v = json!({"name": p.prop, "param": p.ident, "type": p.kind.id(), "default": p.default}); if let Some(vals) = &values { v["value"] = vals.iter().find(|(name, _)| *name == p.prop).map_or(Value::Null, |(_, x)| x.clone()); } v }) .collect(); let mut out = json!({ "component": c.comp, "name": c.type_name, "set": c.set, "properties": properties, "definition": definition(doc, &c, lang), "language": lang.id(), }); if values.is_some() { out["usage"] = json!(usage(doc, id, lang, 0)); } out["connected"] = crate::code_connect::link(doc, id, lang).map_or(Value::Null, |l| { json!({"importPath": l.import_path, "name": l.name, "template": l.template, "import": crate::code_connect::import_line(l, lang), "code": crate::code_connect::connected_code(doc, id, lang, l)}) }); Ok(out) } )] } /// Code for any layer in React, Leptos, Vue, SwiftUI, Compose or ArkUI: an instance's call and its /// component's definition, a component's (or set's) definition, or the layer tree (React and /// Leptos wrap it in a function component), with the imports of the connected components it calls /// at the top. pub fn code(doc: &Document, id: NodeId, lang: CodeLang) -> String { let Some(n) = doc.node(id) else { return String::new() }; if let Some(code) = crate::code_connect::link(doc, id, lang).and_then(|l| crate::code_connect::connected_code(doc, id, lang, l)) { return code; } let comp = main_of(doc, id).and_then(|m| describe(doc, m)); let code = match (&n.kind, comp) { (NodeKind::Instance { .. }, Some(c)) => format!("{}\n{}", usage(doc, id, lang, 0).unwrap_or_default(), definition(doc, &c, lang)), (NodeKind::Component { .. } | NodeKind::ComponentSet { .. }, Some(c)) => definition(doc, &c, lang), _ => match lang { CodeLang::React | CodeLang::Leptos | CodeLang::Vue => { let c = Component { comp: id, set: None, type_name: component_type_name(&n.name), params: Vec::new() }; match lang { CodeLang::Leptos => leptos(doc, &c), CodeLang::Vue => vue(doc, &c), _ => react(doc, &c), } } CodeLang::SwiftUi => { let mut out = String::new(); swift(&Ctx::new(doc, id), id, 0, &mut out, &[]); out } CodeLang::ArkUi => { let mut out = String::new(); arkui(&Ctx::new(doc, id), id, 0, &mut out, &[]); out } _ => { let mut out = String::new(); compose(&Ctx::new(doc, id), id, 0, &mut out); out } }, }; let imports = crate::code_connect::tree_imports(doc, id, lang); if lang == CodeLang::Vue && !imports.is_empty() { return vue_with_imports(&imports, code); } crate::code_connect::with_imports(&imports, code) } /// A Vue single-file component imports in its `\n\n{tail}") } None => crate::code_connect::with_imports(imports, code), } } fn is_component(doc: &Document, id: NodeId) -> bool { doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Component { .. })) } /// The main component a layer's code is defined by: a component itself, an instance's main, and /// for anything in a set the set's default (top-left) variant. pub fn main_of(doc: &Document, id: NodeId) -> Option { let n = doc.node(id)?; let main = match &n.kind { NodeKind::Component { .. } => id, NodeKind::ComponentSet { .. } => return crate::components::default_variant(doc, id), NodeKind::Instance { component, .. } => *component, _ => return None, }; if !is_component(doc, main) { return None; } match variant_set(doc, main) { Some(set) => crate::components::default_variant(doc, set), None => Some(main), } } /// `comp` (a component) as code. pub fn describe(doc: &Document, comp: NodeId) -> Option { let n = doc.node(comp)?; let NodeKind::Component { props, .. } = &n.kind else { return None }; let set = variant_set(doc, comp); let type_name = component_type_name(&set.and_then(|s| doc.node(s)).unwrap_or(n).name); let mut used: Vec = Vec::new(); let mut enums: Vec = vec![type_name.clone()]; let mut params: Vec = Vec::new(); let mut add = |prop: String, kind: ParamKind, default: Value| { let enum_name = if matches!(kind, ParamKind::Variant(_)) { enum_ident(&prop, &mut enums) } else { String::new() }; params.push(Param { ident: ident(&prop, &mut used), prop, kind, default, enum_name }); }; let mut seen: Vec = Vec::new(); if let Some(set) = set { let own = variant_props(&n.name); for (k, vals) in variant_options(doc, set) { let default = own.get(&k).or_else(|| vals.first()).cloned().unwrap_or_default(); seen.push(k.clone()); add(k, ParamKind::Variant(vals), Value::String(default)); } } for (k, d) in props { if seen.contains(k) { continue; } let kind = match d { PropDef::Boolean { .. } => ParamKind::Boolean, PropDef::Text { .. } => ParamKind::Text, PropDef::InstanceSwap { .. } => ParamKind::InstanceSwap, PropDef::Variant { options, .. } => ParamKind::Variant(options.clone()), PropDef::Slot { .. } => ParamKind::Slot, }; add(k.clone(), kind, prop_default_value(d)); } Some(Component { comp, set, type_name, params }) } // ------------------------------------------------------------------ names /// ASCII words of a name ("Show icon?" → ["Show", "icon"]). fn words(s: &str) -> Vec<&str> { s.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()).take(12).collect() } fn capitalized(w: &str) -> String { let mut c = w.chars(); c.next().map(|f| f.to_ascii_uppercase().to_string() + c.as_str()).unwrap_or_default() } /// A type name: "Button / Primary" → `ButtonPrimary`. pub fn type_name(name: &str) -> String { let t: String = words(name).into_iter().map(capitalized).collect::().chars().take(60).collect(); match t.chars().next() { None => "Component".into(), Some(c) if c.is_ascii_digit() => format!("Component{t}"), Some(_) => t, } } /// Words no generated parameter may be called (keywords of TypeScript, Swift and Kotlin, and /// names the generated code uses). const RESERVED: &[&str] = &[ "arguments", "as", "await", "body", "break", "case", "catch", "class", "const", "continue", "default", "defer", "delete", "do", "else", "enum", "eval", "export", "extends", "extension", "false", "for", "fun", "func", "function", "guard", "if", "implements", "import", "in", "init", "interface", "is", "key", "let", "modifier", "new", "nil", "null", "object", "operator", "package", "private", "protected", "protocol", "public", "ref", "repeat", "return", "self", "static", "struct", "super", "switch", "this", "throw", "true", "try", "typealias", "typeof", "val", "var", "void", "when", "where", "while", "with", "yield", ]; /// Types the generated SwiftUI, Compose and ArkUI code names: a variant enum may not shadow them. const TYPES: &[&str] = &[ "Alignment", "AnyView", "Arrangement", "Bool", "Boolean", "Box", "CaseIterable", "Circle", "CircleShape", "Color", "Column", "Composable", "Edge", "EdgeInsets", "Ellipse", "EmptyView", "FlexAlign", "Font", "FontStyle", "FontWeight", "Group", "HStack", "HorizontalAlign", "Image", "Modifier", "Rect", "RectangleShape", "RoundedCornerShape", "RoundedRectangle", "Row", "Shape", "Stack", "String", "Text", "TextAlign", "TextCase", "TextDecorationType", "Unit", "VStack", "VerticalAlign", "View", "ZStack", ]; /// A component's name in code: [`type_name`], suffixed when it would shadow one of [`TYPES`] /// ("Text" → `TextComponent`). pub fn component_type_name(name: &str) -> String { let t = type_name(name); if TYPES.contains(&t.as_str()) { format!("{t}Component") } else { t } } /// A variant enum's name: "Size" → `Size`, unique in `used`, never one of [`TYPES`]. fn enum_ident(prop: &str, used: &mut Vec) -> String { let t = type_name(prop); let base = if t == "Component" || TYPES.contains(&t.as_str()) { format!("{t}Variant") } else { t }; let out = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}{k}") }).find(|n| !used.contains(n)).unwrap_or(base); used.push(out.clone()); out } /// A parameter name: "Show icon" → `showIcon`, unique in `used`. fn ident(name: &str, used: &mut Vec) -> String { let mut base = String::new(); for (i, w) in words(name).into_iter().enumerate() { base.push_str(&if i == 0 { w.to_ascii_lowercase() } else { capitalized(w) }); } let mut base: String = base.chars().take(40).collect(); if base.is_empty() { base = "prop".into(); } else if base.starts_with(|c: char| c.is_ascii_digit()) { base = format!("p{base}"); } if RESERVED.contains(&base.as_str()) { base.push_str("Value"); } let out = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}{k}") }).find(|n| !used.contains(n)).unwrap_or(base); used.push(out.clone()); out } /// Rust's keywords, and `children` (Leptos passes a component's children by that name). const RUST_RESERVED: &[&str] = &[ "abstract", "as", "async", "await", "become", "box", "break", "children", "const", "continue", "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", ]; /// A parameter's Rust name: its camelCase name in snake_case (`showIcon` → `show_icon`), with a /// trailing `_` when Rust reserves it (`type_`; camelCase names have no `_`, so it stays unique). pub(crate) fn rust_ident(ident: &str) -> String { let mut out = String::new(); for (i, ch) in ident.chars().enumerate() { if ch.is_ascii_uppercase() { if i > 0 { out.push('_'); } out.push(ch.to_ascii_lowercase()); } else { out.push(ch); } } if RUST_RESERVED.contains(&out.as_str()) { out.push('_'); } out } /// A Rust type name: `Self` is taken. fn rust_type(t: &str) -> String { if t == "Self" { "SelfComponent".into() } else { t.to_string() } } /// A Leptos variant enum's name: the component's name and the property's, never the props struct /// `#[component]` makes (`ButtonProps`). fn leptos_enum(c: &Component, p: &Param) -> String { let t = rust_type(&c.type_name); let name = format!("{t}{}", p.enum_name); if name == format!("{t}Props") { format!("{name}Variant") } else { name } } /// Rust enum variants for variant values: [`cases`] in PascalCase, `Self` taken. fn rust_cases(values: &[String]) -> Vec { cases(values, false).into_iter().map(|c| if c == "Self" { "SelfValue".into() } else { c }).collect() } /// Enum case names for variant values, unique: Swift's lowerCamel (`.small`, `` .`default` ``), /// Kotlin's PascalCase (`Small`). fn cases(values: &[String], swift: bool) -> Vec { let mut used = Vec::new(); values .iter() .map(|v| { if swift { let id = ident(v, &mut used); // Keyword cases stay readable in backticks rather than suffixed. match id.strip_suffix("Value").filter(|b| RESERVED.contains(b)) { Some(b) => format!("`{b}`"), None => id, } } else { let t = type_name(v); let t = if t == "Component" && !v.is_empty() && words(v).is_empty() { "Value".to_string() } else { t }; let out = (1..).map(|k| if k == 1 { t.clone() } else { format!("{t}{k}") }).find(|n| !used.contains(n)).unwrap_or(t); used.push(out.clone()); out } }) .collect() } /// A Rust string literal. pub(crate) fn rust_string(s: &str) -> String { format!("{:?}", s.chars().take(10_000).collect::()) } /// A JavaScript string literal. pub(crate) fn js_string(s: &str) -> String { let s: String = s.chars().take(10_000).collect(); serde_json::to_string(&s).unwrap_or_else(|_| "\"\"".into()) } /// A JavaScript string literal safe inside a Vue single-file component: `<` is escaped, so a /// default containing `` can't close the `\n\n"); } out.push_str(&format!("\n\n\n")); out } /// A Leptos `#[component]` with its variant enums and props (defaults as `#[prop]` attributes), /// and its stylesheet. fn leptos(doc: &Document, c: &Component) -> String { let (root_class, markup, css) = web(doc, c, CodeLang::Leptos); let mut out = String::from("use leptos::prelude::*;\n\n"); for p in &c.params { let ParamKind::Variant(opts) = &p.kind else { continue }; let (e, cases) = (leptos_enum(c, p), rust_cases(opts)); let default = opts.iter().position(|o| Some(o.as_str()) == p.default.as_str()).unwrap_or(0); out.push_str(&format!("#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]\npub enum {e} {{\n")); for (i, case) in cases.iter().enumerate() { out.push_str(&format!("{} {case},\n", if i == default { " #[default]\n" } else { "" })); } out.push_str(&format!("}}\n\nimpl {e} {{\n /// The variant's name, as its `data-*` attribute.\n pub fn as_str(self) -> &'static str {{\n match self {{\n")); for (o, case) in opts.iter().zip(&cases) { out.push_str(&format!(" Self::{case} => {},\n", rust_string(o))); } out.push_str(" }\n }\n}\n\n"); } let t = rust_type(&c.type_name); out.push_str(&format!("#[component]\npub fn {t}(")); if !c.params.is_empty() { out.push('\n'); for p in &c.params { let default = value_expr(doc, c, p, &p.default, CodeLang::Leptos); let (attr, ty) = match &p.kind { ParamKind::Variant(_) => ("optional".to_string(), leptos_enum(c, p)), ParamKind::Boolean => (format!("default = {default}"), "bool".into()), ParamKind::Text => (format!("into, default = {default}.to_string()"), "String".into()), ParamKind::InstanceSwap => (format!("into, default = ({default}).into()"), "ViewFn".into()), ParamKind::Slot => ("optional, into".to_string(), "Option".into()), }; out.push_str(&format!(" #[prop({attr})] {}: {ty},\n", rust_ident(&p.ident))); } } out.push_str(&format!(") -> impl IntoView {{\n view! {{\n{markup} }}\n}}\n\n/* {root_class}.css */\n{css}")); out } /// A SwiftUI view struct: variant enums, stored properties with defaults, and the body. fn swift_definition(doc: &Document, c: &Component) -> String { let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 2 }; let mut out = format!("struct {}: View {{\n", c.type_name); for p in &c.params { if let ParamKind::Variant(opts) = &p.kind { out.push_str(&format!(" enum {}: String, CaseIterable {{\n", enum_name(c, p, true))); for (o, case) in opts.iter().zip(cases(opts, true)) { out.push_str(&format!(" case {case} = {}\n", literal(o, false))); } out.push_str(" }\n"); } } for p in &c.params { let ty = match &p.kind { ParamKind::Variant(_) => enum_name(c, p, true), ParamKind::Boolean => "Bool".into(), ParamKind::Text => "String".into(), ParamKind::InstanceSwap => "AnyView".into(), ParamKind::Slot => "AnyView?".into(), }; let default = if p.kind == ParamKind::Slot { "nil".into() } else { value_expr(doc, c, p, &p.default, CodeLang::SwiftUi) }; out.push_str(&format!(" var {}: {ty} = {default}\n", p.ident)); } if !c.params.is_empty() { out.push('\n'); } out.push_str(" var body: some View {\n"); swift(&ctx, c.comp, 2, &mut out, &[]); out.push_str(" }\n}\n"); out } /// A `@Composable` function: variant enum classes, parameters with defaults, and the body. fn compose_definition(doc: &Document, c: &Component) -> String { let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 1 }; let mut out = String::new(); for p in &c.params { if let ParamKind::Variant(opts) = &p.kind { out.push_str(&format!("enum class {} {{ {} }}\n\n", enum_name(c, p, false), cases(opts, false).join(", "))); } } out.push_str(&format!("@Composable\nfun {}(", c.type_name)); if !c.params.is_empty() { out.push('\n'); for p in &c.params { let ty = match &p.kind { ParamKind::Variant(_) => enum_name(c, p, false), ParamKind::Boolean => "Boolean".into(), ParamKind::Text => "String".into(), ParamKind::InstanceSwap => "@Composable () -> Unit".into(), ParamKind::Slot => "(@Composable () -> Unit)?".into(), }; let default = if p.kind == ParamKind::Slot { "null".into() } else { value_expr(doc, c, p, &p.default, CodeLang::Compose) }; out.push_str(&format!(" {}: {ty} = {default},\n", p.ident)); } } out.push_str(") {\n"); compose(&ctx, c.comp, 1, &mut out); out.push_str("}\n"); out } /// An ArkUI (`@Component`) struct: variant enums, `@Prop` parameters, a default builder for each /// instance swap and slot, and the `build` body. fn arkui_definition(doc: &Document, c: &Component) -> String { let ctx = Ctx { doc, root: c.comp, comp: Some(c), base: 1 }; let mut out = String::new(); for p in &c.params { if let ParamKind::Variant(opts) = &p.kind { out.push_str(&format!("export enum {} {{ {} }}\n\n", enum_name(c, p, false), cases(opts, false).join(", "))); } } // A default builder per instance swap or slot, named apart from every parameter. let mut used: Vec = c.params.iter().map(|p| p.ident.clone()).collect(); let mut builders: Vec<(String, String)> = Vec::new(); for p in &c.params { if !matches!(p.kind, ParamKind::InstanceSwap | ParamKind::Slot) { continue; } let base = format!("default{}Builder", capitalized(&p.ident)); let name = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}{k}") }).find(|n| !used.contains(n)).unwrap_or(base); used.push(name.clone()); builders.push((p.ident.clone(), name)); } out.push_str("@Component\nexport struct "); out.push_str(&c.type_name); out.push_str(" {\n"); for p in &c.params { match &p.kind { ParamKind::Variant(_) => { out.push_str(&format!(" @Prop {}: {} = {}\n", p.ident, enum_name(c, p, false), value_expr(doc, c, p, &p.default, CodeLang::ArkUi))) } ParamKind::Boolean => out.push_str(&format!(" @Prop {}: boolean = {}\n", p.ident, p.default.as_bool().unwrap_or(false))), ParamKind::Text => out.push_str(&format!(" @Prop {}: string = {}\n", p.ident, value_expr(doc, c, p, &p.default, CodeLang::ArkUi))), ParamKind::InstanceSwap | ParamKind::Slot => { let name = builders.iter().find(|(ident, _)| *ident == p.ident).map_or("", |(_, name)| name.as_str()); out.push_str(&format!(" @BuilderParam {}: () => void = this.{name}\n", p.ident)); } } } for (ident, name) in &builders { let Some(p) = c.params.iter().find(|p| p.ident == *ident) else { continue }; out.push_str(&format!("\n @Builder\n {name}() {{\n")); match &p.kind { ParamKind::InstanceSwap => { if let Some(t) = id_of(&p.default).and_then(|i| component_type(doc, i)) { out.push_str(&format!(" {t}()\n")); } } ParamKind::Slot => { let layer = slots_of(doc, c.comp).into_iter().find(|(_, slot)| slot == &p.prop).map(|(layer, _)| layer); let kids = layer.and_then(|l| doc.node(l)).map(|n| ctx.kids(n)).unwrap_or_default(); for k in kids { arkui(&ctx, k, 2, &mut out, &[]); } } _ => {} } out.push_str(" }\n"); } out.push_str("\n build() {\n"); arkui(&ctx, c.comp, 2, &mut out, &[]); out.push_str(" }\n}\n"); out }