//! 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::PenNode; use jian_ops_schema::PenDocument; /// HTML + inline-CSS generator. Walks the document's node tree and /// emits `
` per Rect/Frame/Group with absolute positioning and /// inline style; ellipses get `border-radius: 50%`; text nodes emit /// ``. Bare minimum useful — refining to semantic tags + flex /// containers is a follow-up. pub struct Html; impl Codegen for Html { fn target_label(&self) -> &'static str { "html" } fn generate(&self, doc: &PenDocument) -> String { let mut out = String::new(); out.push_str("\n"); out.push_str("\n\n\n"); for node in root_nodes(doc) { emit_node_html(&mut out, node, 1); } out.push_str("\n"); out } } fn emit_node_html(out: &mut String, node: &PenNode, depth: usize) { if node_hidden(node) { return; } let indent = " ".repeat(depth); let tag = if node_is_text(node) { "span" } else { "div" }; let (x, y) = node_origin(node); let (w, h) = node_size(node); let mut style = format!( "position:absolute;left:{}px;top:{}px;width:{}px;height:{}px", fmt_num(x), fmt_num(y), fmt_num(w), fmt_num(h), ); if let Some(fill) = node_fill_css(node) { style.push_str(&format!(";background:{}", color_to_css(&fill))); } if let Some((color, width)) = node_stroke_css(node) { style.push_str(&format!( ";border:{}px solid {}", width, color_to_css(&color) )); } let radius = node_corner_radius(node); if radius > 0.0 { style.push_str(&format!(";border-radius:{}px", fmt_num(radius))); } if node_is_ellipse(node) { style.push_str(";border-radius:50%"); } let rotation = node_rotation_deg(node); if rotation.abs() > f64::EPSILON { style.push_str(&format!(";transform:rotate({}deg)", fmt_num(rotation))); } let body = node_text(node).unwrap_or_default(); let children = node_children(node); if children.is_empty() && body.is_empty() { out.push_str(&format!("{indent}<{tag} style=\"{style}\">\n")); } else { out.push_str(&format!("{indent}<{tag} style=\"{style}\">")); if !body.is_empty() { out.push_str(&html_escape(&body)); } if !children.is_empty() { out.push('\n'); for c in children { emit_node_html(out, c, depth + 1); } out.push_str(&indent); } out.push_str(&format!("\n")); } } /// Vue 3 Single File Component generator. Wraps the node tree in a /// `