/// 导出管道:从 Project 生成可直接使用的代码/原型。
///
/// - export_html:自包含交互式 HTML 原型(无依赖,可直接打开)
/// - export_react:React TSX 组件桩(内联样式,可直接放进 Next.js)
/// - export_tailwind:Tailwind CSS class 映射(可组合进现有项目)

// ---------------------------------------------------------------------------
// HTML 原型导出
// ---------------------------------------------------------------------------

///|
/// 入场画板推断:导航流的天然起点——没有被任何 flow 指向的画板;并列/成环
/// 时按确定性 tie-break 取最小 id(String < 为长度优先的 shortlex,非纯
/// 字典序)。登录→首页型原型因此开在登录页,不再随
/// Map 遍历序漂移到任意画板(精致感缺陷:开屏即错页)。
fn Project::infer_entry(self : Project) -> String {
  let targets : Map[String, Bool] = {}
  for e in self.flows.iter() {
    targets.set(e.to, true)
  }
  let mut best = ""
  for id in self.artboards.keys() {
    if !targets.contains(id) && (best == "" || id < best) {
      best = id
    }
  }
  if best == "" {
    for id in self.artboards.keys() {
      if best == "" || id < best {
        best = id
      }
    }
  }
  best
}

///|
/// 导出面的 JS 字符串转义(仅 export 层收口,不动 core/json.mbt 的
/// json_escape——那是 JSON 协议语义):boot/tab onclick 落在单引号 JS
/// 字符串、MV_ACT/MV_STATES 落在双引号 JS 字符串,且全部内嵌在  从字符串内提前闭合(含大小写变体)。
fn js_string_escape(s : String) -> String {
  let sb = StringBuilder::new()
  for c in s.iter() {
    match c {
      '"' => sb.write_string("\\x22")
      '\'' => sb.write_string("\\'")
      '\\' => sb.write_string("\\\\")
      '<' => sb.write_string("\\x3c")
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      _ => {
        let code = c.to_int()
        if code < 32 {
          // 其余 C0 控制符:\u00xx(编码尾段与 json_escape 共用)
          write_c0_u_escape(sb, code)
        } else {
          sb.write_char(c)
        }
      }
    }
  }
  sb.to_string()
}

///|
/// 导出为自包含 HTML(SVG 内嵌 + 画板切换 + 导航流模拟)
pub fn Project::export_html(self : Project) -> String {
  let sb = StringBuilder::new()
  sb.write_string(
    "\n\n\n\n",
  )
  sb.write_string(
    "\n",
  )
  // 暗底原型:声明 color-scheme,浏览器控件/滚动条不再闪白
  sb.write_string("\n")
  sb.write_string("MoonViz Prototype\n")
  sb.write_string("\n\n\n")
  // Tab 栏
  sb.write_string("
\n") for id in self.artboards.keys() { match self.artboards.get(id) { Some(ab) => { // onclick 的 id 落在单引号 JS 字符串里,必须转义;DOM id 属性保持 // 原样——getElementById('tab-'+id) 按 JS 解析后的串查 DOM,两侧一致 let js_id = js_string_escape(id) sb.write_string( "
\{ab.name}
\n", ) } None => () } } sb.write_string("
\n") // 画板内容 for id in self.artboards.keys() { match self.artboards.get(id) { Some(ab) => { let svg = render_svg( ab.doc, surface=self.tokens.resolve_color("surface"), frame=self.tokens.resolve_color("outline"), ) sb.write_string( "
\n", ) sb.write_string(svg) sb.write_string("\n
\n") } None => () } } // JS sb.write_string("
\n") sb.write_string("\n\n") sb.to_string() } // --------------------------------------------------------------------------- // React 组件桩导出 // --------------------------------------------------------------------------- ///| /// 导出为 React TSX 组件桩(每个画板一个组件,内联样式) pub fn Project::export_react(self : Project) -> String { let sb = StringBuilder::new() sb.write_string("// Auto-generated by MoonViz Agent Engine\n") sb.write_string("// 每个 Artboard 映射为一个 React 组件\n\n") for id in self.artboards.keys() { match self.artboards.get(id) { Some(ab) => { let comp_name = to_pascal_case(ab.name) sb.write_string("export function \{comp_name}() {\n") sb.write_string(" return (\n") // 生成 JSX 树 let layout = solve(ab.doc) write_react_node(sb, ab.doc, ab.doc.root_id, layout, 2) sb.write_string(" )\n}\n\n") } None => () } } // 导航流映射 if self.flows.length() > 0 { sb.write_string("// Navigation flows:\n") for e in self.flows.iter() { sb.write_string( "// \{to_pascal_case(e.from)} -> \{to_pascal_case(e.to)} (trigger: \{e.trigger})\n", ) } } sb.to_string() } ///| fn write_react_node( sb : StringBuilder, doc : Document, id : String, layout : SolvedLayout, indent : Int, ) -> Unit { let pad = repeat_str(" ", indent) match (doc.nodes.get(id), layout.rect_of(id)) { (Some(n), Some(r)) => { let style = "position:'absolute',left:\{r.x.to_int()},top:\{r.y.to_int()},width:\{r.width.to_int()},height:\{r.height.to_int()}" let extra_style = react_style(n) let full_style = if extra_style != "" { "\{style},\{extra_style}" } else { style } match n.kind { Text => sb.write_string( "\{pad}\{jsx_text_escape(n.text)}\n", ) _ => if n.children.length() == 0 { sb.write_string( "\{pad}
\n", ) } else { sb.write_string( "\{pad}
\n", ) for cid in n.children.iter() { write_react_node(sb, doc, cid, layout, indent + 1) } sb.write_string("\{pad}
\n") } } } _ => () } } ///| /// JSX 文本内容转义:& < > { } 按实体转义,防止文本字面量破坏标签/表达式结构 fn jsx_text_escape(s : String) -> String { let sb = StringBuilder::new() for c in s.iter() { match c { '&' => sb.write_string("&") '<' => sb.write_string("<") '>' => sb.write_string(">") '{' => sb.write_string("{") '}' => sb.write_string("}") _ => sb.write_char(c) } } sb.to_string() } ///| fn react_style(n : Node) -> String { let parts : Array[String] = [] if n.style.fill != "none" && n.style.fill != "" { parts.push("background:'\{n.style.fill}'") } if n.style.corner_radius > 0 { parts.push("borderRadius:\{n.style.corner_radius.to_int()}") } if n.style.font_size != 14.0 { parts.push("fontSize:\{n.style.font_size.to_int()}") } if n.style.text_color != "#111111" { parts.push("color:'\{n.style.text_color}'") } if n.style.stroke != "none" && n.style.stroke != "" { parts.push("border:'1px solid \{n.style.stroke}'") } jstr8(parts, ",") } ///| fn jstr8(parts : Array[String], sep : String) -> String { let sb = StringBuilder::new() for i in 0.. 0 { sb.write_string(sep) } sb.write_string(parts[i]) } sb.to_string() } // --------------------------------------------------------------------------- // 工具 // --------------------------------------------------------------------------- ///| fn to_pascal_case(s : String) -> String { let sb = StringBuilder::new() let mut upper_next = true for c in s.iter() { let code = c.to_int() if code == 95 || code == 45 || code == 32 { // _ - space upper_next = true } else if upper_next { if code >= 97 && code <= 122 { sb.write_char(Char::from_int(code - 32)) } else { sb.write_char(c) } upper_next = false } else { sb.write_char(c) } } sb.to_string() } ///| fn repeat_str(s : String, n : Int) -> String { let sb = StringBuilder::new() for _i in 0..