//! Per-framework markup generators — the React / Vue / Svelte / HTML //! / Flutter / SwiftUI / Compose / React Native targets carved off //! `lib.rs` to keep both files under the 800-line cap. //! //! All generators consume the canonical `jian_ops_schema::PenDocument` //! and walk `PenNode` through the flat-view accessors in the crate //! root (`node_origin` / `node_size` / `node_fill_css` / //! …) so the per-target emit logic stays variant-agnostic. use crate::{ color_to_css, fmt_num, html_escape, node_children, node_corner_radius, node_fill_css, node_hidden, node_is_ellipse, node_is_text, node_origin, node_rotation_deg, node_size, node_stroke_css, node_text, parse_color, root_nodes, Codegen, CssVariables, }; use jian_ops_schema::node::{BoolOrExpression, NumberOrExpression, PenNode}; use jian_ops_schema::PenDocument; // --- First-class widget element mapping ---------------------------- // // The 10 form/widget node kinds (text_input / text_area / number_input // / select / radio_group / switch / checkbox / slider / progress / // tabs) map to real HTML form elements instead of falling through to // the generic `
` / `` heuristic in `emit_node_html` / // `emit_node_jsx`. These helpers return the inner element markup; the // per-target emitters keep owning the positioned wrapper. Generated // markup is valid in both HTML and JSX (self-closed voids end `/>`, // boolean attributes are bare — which JSX accepts), so the two targets // share one mapping. `tabs` is the only container: it emits a button // tablist from `tabs[]` and recurses children as panels, so its inner // recursion is target-specific and handled inline by the caller. /// Flatten a `BoolOrExpression` to a literal `true`. Expression-bound /// checked states can't be resolved statically, so they read as /// unchecked — same degrade the other generators apply for `$var`. fn bool_or_expr_true(b: &Option) -> bool { matches!(b, Some(BoolOrExpression::Bool(true))) } /// Flatten a `NumberOrExpression` to its literal value, when concrete. /// Expression-bound values read as `None` (no static value attribute). fn number_or_expr(n: &Option) -> Option { match n { Some(NumberOrExpression::Number(v)) => Some(*v), _ => None, } } /// Emit a single HTML attribute `key="value"` (value html-escaped), /// skipping when `value` is `None`. The leading space lets callers /// concatenate attributes onto an open tag. fn opt_attr(key: &str, value: Option<&str>) -> String { match value { Some(v) => format!(" {}=\"{}\"", key, html_escape(v)), None => String::new(), } } /// Emit a numeric attribute `key="n"` (formatted via `fmt_num`), /// skipping when `value` is `None`. fn opt_num_attr(key: &str, value: Option) -> String { match value { Some(v) => format!(" {}=\"{}\"", key, fmt_num(v)), None => String::new(), } } /// Map a first-class widget node to its form-element markup. Returns /// `None` for non-widget variants (so the caller falls back to the /// generic div/span heuristic). `tabs` is handled by the caller /// because its panel children recurse target-specifically; this /// helper covers the 9 leaf widget kinds. fn widget_markup(node: &PenNode) -> Option { Some(match node { PenNode::TextInput(n) => format!( "", opt_attr("placeholder", n.placeholder.as_deref()), opt_attr("value", n.value.as_deref()), ), PenNode::TextArea(n) => format!( "{}", opt_attr("placeholder", n.placeholder.as_deref()), html_escape(n.value.as_deref().unwrap_or_default()), ), PenNode::NumberInput(n) => format!( "", opt_num_attr("min", n.min), opt_num_attr("max", n.max), opt_num_attr("step", n.step), opt_num_attr("value", number_or_expr(&n.value)), ), PenNode::Select(n) => { let mut s = String::from(""); s } PenNode::RadioGroup(n) => { let group = n.base.id.as_str(); let mut s = String::new(); for opt in n.options.iter().flatten() { let checked = if n.value.as_deref() == Some(opt.value.as_str()) { " checked" } else { "" }; s.push_str(&format!( "", html_escape(group), html_escape(&opt.value), checked, html_escape(&opt.label), )); } s } PenNode::Switch(n) => format!( "", if bool_or_expr_true(&n.checked) { " checked" } else { "" }, ), PenNode::Checkbox(n) => format!( " ", if bool_or_expr_true(&n.checked) { " checked" } else { "" }, html_escape(n.label.as_deref().unwrap_or_default()), ), PenNode::Slider(n) => format!( "", opt_num_attr("min", n.min), opt_num_attr("max", n.max), opt_num_attr("step", n.step), opt_num_attr("value", number_or_expr(&n.value)), ), PenNode::Progress(n) => format!( "", opt_num_attr("value", number_or_expr(&n.value)), opt_num_attr("max", n.max), ), _ => return None, }) } /// Emit the tablist `