///|
/// 用户自定义组件系统(docs/11-user-components.md)。
///
/// 引擎侧只做「注册功能」:不区分声明来自 Agent(自然语言翻译)还是
/// 人类(Studio 画布沉淀)——落地产物统一是 `.mbt.md` 组件声明。
/// 组件在引擎内部是内存注册表 + 快照协议;物理持久化由宿主负责
/// (Node SDK / Studio / DeepOrca),引擎保持零文件 IO 的既有纪律。
///
/// 组件对外分发的唯一形态是 MCF 专有格式(core/mcf.mbt,字节布局见
/// docs/11 §3):`.mbt.md` 源码只存在于引擎内部与本地库,不出现在对外流转中。
///|
pub(all) struct UserParam {
name : String
kind : String // "text" | "color" | "number"
default_value : String
}
///|
/// 用户组件:带参数槽的节点树模板。
/// `doc` 是组件画板(相对坐标);实例化 = 深拷贝 + 参数代入 + 放置。
pub(all) struct UserComponentDef {
id : String
description : String
category : String // "user" | "project"(来源标注,宿主决定)
params : Array[UserParam]
/// 变体 = 参数预设覆盖表(variant name -> param -> value)
variants : Map[String, Map[String, String]]
default_variant : String
width : Double
height : Double
doc : Document
/// 声明源码(canonical,引擎内部持有;对外仅经 MCF)
source : String
}
///|
/// 参数表:name -> value(已含默认值合并与变体覆盖)。
/// 未知 variant 与未知参数名报错(拼写错误不再静默吞掉)。
fn resolve_args(
def : UserComponentDef,
variant : String,
overrides : Map[String, String],
) -> Result[Map[String, String], String] {
let out : Map[String, String] = Map::new()
for p in def.params.iter() {
out.set(p.name, p.default_value)
}
let v = if variant == "" { def.default_variant } else { variant }
// default_variant 本身始终合法(零变体组件的回退);显式传入的
// 其他未知变体名才拒绝(拼写错误不再静默吞掉)
if v != def.default_variant && def.variants.contains(v) == false {
// 错误即文档(对齐 constrain 词表先例):variant 槽最常被漏 '-' 占位,
// 坐标串滑进来当变体名——就地携带合法变体与语法模板,模型一轮自纠
let mut names = ""
for k in def.variants.keys() {
names = names + k + " "
}
return Err(
"unknown_variant:\{v}(组件 \{def.id} 合法变体:\{names}默认:\{def.default_variant})——place 语法:place <板> <组件> [variant|-] [x] [y] [w] [h] [k=v...],不需要变体时该槽写 -",
)
}
match def.variants.get(v) {
Some(overrides_v) =>
for k in overrides_v.keys() {
match overrides_v.get(k) {
Some(value) => out.set(k, value)
None => ()
}
}
None => ()
}
for k in overrides.keys() {
match overrides.get(k) {
Some(value) => {
if out.contains(k) == false {
return Err("unknown_param:" + k)
}
out.set(k, value)
}
None => ()
}
}
Ok(out)
}
///|
/// 逐字符比较 s[at..at+name.length()] 是否等于 name(避免 failable 切片)
fn str_eq_at(s : String, at : Int, name : String) -> Bool {
if at < 0 || at + name.length() > s.length() {
return false
}
for j = 0; j < name.length(); j = j + 1 {
if s[at + j] != name[j] {
return false
}
}
true
}
///|
/// `${name}` 参数代入(单遍扫描式):遇到 `${` 时按参数名长度降序做
/// 最长匹配,命中即写出参数值并跳过;替换结果不再参与后续匹配——
/// 替换顺序确定(与 Map 迭代序无关),参数值内嵌 `${other}` 不递归展开。
fn subst_str(text : String, args : Map[String, String]) -> String {
if text.contains("${") == false {
return text
}
let names : Array[String] = []
for n in args.keys() {
names.push(n)
}
// 长名优先,避免前缀名吞掉长名(cart 与 cart_icon 并存时)
names.sort_by(fn(a, b) { b.length() - a.length() })
let sb = StringBuilder::new()
let n = text.length()
let mut i = 0
let mut matched = true
while i < n {
matched = false
if text[i].to_int() == 36 && i + 2 < n && text[i + 1].to_int() == 123 {
for name in names.iter() {
let nl = name.length()
if i + 2 + nl < n &&
str_eq_at(text, i + 2, name) &&
text[i + 2 + nl].to_int() == 125 {
match args.get(name) {
Some(v) => {
sb.write_string(v)
i = i + 3 + nl
matched = true
break
}
None => ()
}
}
}
}
if matched == false {
sb.write_char(text[i].to_int().unsafe_to_char())
i = i + 1
}
}
sb.to_string()
}
///|
fn subst_node(n : Node, args : Map[String, String]) -> Node {
let mut style = n.style
style = {
..style,
fill: subst_str(style.fill, args),
text_color: subst_str(style.text_color, args),
stroke: subst_str(style.stroke, args),
}
{ ..n, text: subst_str(n.text, args), style, }
}
///|
/// 回滚本次放置已插入的节点(账本逆序:子先父后),恢复父 children。
fn rollback_inserted(
target : Document,
inserted : Array[(String, String)],
) -> Unit {
let mut j = inserted.length() - 1
while j >= 0 {
let (host_id, host_parent) = inserted[j]
match target.nodes.get(host_parent) {
Some(parent) => {
let kids : Array[String] = []
for k in parent.children.iter() {
if k != host_id {
kids.push(k)
}
}
target.content_seq = target.content_seq + 1
target.nodes.set(host_parent, { ..parent, children: kids, })
}
None => ()
}
target.content_seq = target.content_seq + 1
target.nodes.remove(host_id)
j = j - 1
}
}
///|
/// 深拷贝组件树到目标画板:BFS 按依赖序(父先子后)insert。
/// 实例节点 id = `::<原id>`;顶层节点以 (dx, dy) 为组件
/// 包围盒原点做绝对放置(首个顶层节点落在 (dx,dy),其余保持相对结构,
/// 与内置组件的绝对落点语义一致)。
/// 任一步失败时回滚已插入节点,目标画板保持调用前状态。
/// 返回插入的顶层根节点 id。
pub fn place_user_component(
target : Document,
def : UserComponentDef,
instance_id : String,
variant? : String = "",
args? : Map[String, String] = Map::new(),
dx? : Double = 0.0,
dy? : Double = 0.0,
) -> Result[String, String] {
if target.has(instance_id) {
return Err("duplicate_node:" + instance_id)
}
let resolved = match resolve_args(def, variant, args) {
Ok(m) => m
Err(e) => return Err(e)
}
// BFS:组件画板 root 的直接子节点为顶层;逐层展开
let queue : Array[(String, String)] = [] // (组件内节点id, 宿主父id)
match def.doc.nodes.get("root") {
None => return Err("usercomp_missing_root")
Some(root) =>
for cid in root.children.iter() {
queue.push((cid, "root"))
}
}
// 组件包围盒原点:首个顶层节点的组件内坐标
let mut origin_x = 0.0
let mut origin_y = 0.0
let mut origin_set = false
// 回滚账本:(host_id, host_parent) 逆序删除
let inserted : Array[(String, String)] = []
let mut placed_root_id = ""
let mut i = 0
while i < queue.length() {
let (cid, host_parent) = queue[i]
i = i + 1
match def.doc.nodes.get(cid) {
None => {
rollback_inserted(target, inserted)
return Err("usercomp_missing_node:" + cid)
}
Some(node) => {
let host_id = instance_id + "::" + cid
let is_top = host_parent == "root"
let mut n = subst_node(node, resolved)
if is_top {
let base_x = match node.x {
AlignStart(v) => v
_ => 0.0
}
let base_y = match node.y {
AlignStart(v) => v
_ => 0.0
}
if origin_set == false {
origin_x = base_x
origin_y = base_y
origin_set = true
}
// 绝对放置:组件原点对齐 (dx, dy),顶层节点保持相对结构。
// 负坐标夹回 0(与内置 place_component 边界策略一致),
// 越界由 contained_in_parent 谓词显式报告为视觉债。
let abs_x = dx + (base_x - origin_x)
let abs_y = dy + (base_y - origin_y)
let safe_x = if abs_x < 0.0 { 0.0 } else { abs_x }
let safe_y = if abs_y < 0.0 { 0.0 } else { abs_y }
n = { ..n, x: AlignStart(safe_x), y: AlignStart(safe_y), }
if placed_root_id == "" {
placed_root_id = host_id
}
}
// 关键:实例节点 id 必须重写为带实例前缀的 host_id(组件内同名隔离)
n = { ..n, id: host_id, children: [], } // 子关系在下方按序重建
if target.insert_node(n, host_parent) == false {
rollback_inserted(target, inserted)
return Err("usercomp_insert_failed:" + host_id)
}
inserted.push((host_id, host_parent))
for child in node.children.iter() {
queue.push((child, host_id))
}
}
}
}
if placed_root_id == "" {
rollback_inserted(target, inserted)
return Err("usercomp_empty:" + def.id)
}
Ok(placed_root_id)
}
///|
/// 校验辅助:收集节点属性中出现的 `${name}` 引用(compile 阶段闭合检查)。
/// 单遍扫描(每轮 rest 严格前进),无轮数上限。
pub fn collect_param_refs(def : UserComponentDef) -> Array[String] raise {
let refs : Array[String] = []
let known : Map[String, Bool] = Map::new()
for id in def.doc.nodes.keys() {
match def.doc.nodes.get(id) {
None => ()
Some(n) =>
for text in [n.text, n.style.fill, n.style.text_color, n.style.stroke] {
let mut rest = text
let mut scanning = true
while scanning {
match rest.find("${") {
None => scanning = false
Some(start) => {
let after = rest[start + 2:].to_string()
match after.find("}") {
None => scanning = false
Some(end) => {
let name = after[:end].to_string()
if name != "" && known.contains(name) == false {
known.set(name, true)
refs.push(name)
}
rest = after[end + 1:].to_string()
}
}
}
}
}
}
}
}
refs
}