From c79bc04a2969f3a281eb394952a3a1cbcb79b3eb Mon Sep 17 00:00:00 2001 From: phonedu <135960865+phonedu@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:48:21 +0800 Subject: [PATCH] M6: default Vue instance swaps and keep text out of attribute detection A Vue component's instance-swap prop now defaults to its component reference in withDefaults, so the dynamic component renders the design default. Attribute detection only applies inside an unfinished opening tag, so text like `Total = {title}` stays text instead of becoming a quoted attribute value. --- crates/engine/src/code_connect.rs | 28 ++++++++++++++++++++ crates/engine/src/code_connect_tests.rs | 4 +++ crates/engine/src/component_codegen.rs | 5 +--- crates/engine/src/component_codegen_tests.rs | 1 + 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/code_connect.rs b/crates/engine/src/code_connect.rs index 1ba7cc79..07684f64 100644 --- a/crates/engine/src/code_connect.rs +++ b/crates/engine/src/code_connect.rs @@ -184,10 +184,38 @@ fn expr(doc: &Document, c: &Component, p: &Param, v: &Value, lang: CodeLang) -> } } +/// Whether `s` ends inside an unclosed opening tag (no `>` outside quotes since the last `<`). +fn inside_open_tag(s: &str) -> bool { + let mut in_tag = false; + let mut quote = None; + let mut chars = s.chars().peekable(); + while let Some(c) = chars.next() { + match quote { + Some(q) => { + if c == q { + quote = None; + } + } + None => match c { + '<' if !in_tag && chars.peek().is_some_and(|ch| ch.is_ascii_alphabetic() || matches!(*ch, '/' | '!' | '?')) => { + in_tag = true; + } + '>' if in_tag => in_tag = false, + '"' | '\'' if in_tag => quote = Some(c), + _ => {} + }, + } + } + in_tag +} + /// Where a placeholder sits in a Vue template: whether the attribute is bound, and the quote the /// template already opened (if any). `None` when it isn't in an attribute at all. fn vue_attribute(out: &str) -> Option<(bool, Option)> { let end = out.trim_end(); + if !inside_open_tag(end) { + return None; + } let (after, quote) = match end.chars().last() { Some(q @ ('"' | '\'')) => (&end[..end.len() - q.len_utf8()], Some(q)), _ => (end, None), diff --git a/crates/engine/src/code_connect_tests.rs b/crates/engine/src/code_connect_tests.rs index 721908b3..582a0c5e 100644 --- a/crates/engine/src/code_connect_tests.rs +++ b/crates/engine/src/code_connect_tests.rs @@ -237,6 +237,10 @@ fn vue_links_are_a_default_import_with_quoted_bindings() { ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Say \"hi\""}})); ex(&mut s, "codeConnect.set", json!({"id": i, "language": "vue", "template": ""})); assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n\n"); + // Text after a completed tag is text, even when it contains `=`. + ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Title": "Buy"}})); + ex(&mut s, "codeConnect.set", json!({"id": i, "language": "vue", "template": "

Total = {title}

"})); + assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n

Total = Buy

\n"); // Vue is linkable; a language that isn't stays refused. assert!(s.execute("codeConnect.set", &json!({"id": c, "language": "css"})).is_err(), "not a code language"); assert!(s.execute("codeConnect.set", &json!({"id": c, "language": "vue", "importPath": "a b"})).is_err(), "no spaces in a path"); diff --git a/crates/engine/src/component_codegen.rs b/crates/engine/src/component_codegen.rs index 2f164238..00f9e1b6 100644 --- a/crates/engine/src/component_codegen.rs +++ b/crates/engine/src/component_codegen.rs @@ -823,10 +823,7 @@ fn vue(doc: &Document, c: &Component) -> String { } out.push_str(" }>(),\n {\n"); for p in &props { - // A component-valued prop has no literal to default to. - if matches!(p.kind, ParamKind::InstanceSwap | ParamKind::Slot) { - continue; - } + // An instance swap defaults to its component reference; a slot isn't in `props`. out.push_str(&format!(" {}: {},\n", p.ident, value_expr(doc, c, p, &p.default, CodeLang::Vue))); } out.push_str(" },\n);\n\n\n\n"); diff --git a/crates/engine/src/component_codegen_tests.rs b/crates/engine/src/component_codegen_tests.rs index f8f14254..2035bdac 100644 --- a/crates/engine/src/component_codegen_tests.rs +++ b/crates/engine/src/component_codegen_tests.rs @@ -228,6 +228,7 @@ fn instance_swap_properties_take_views() { assert!(leptos.contains("{icon.run()}"), "{leptos}"); let vue = code(&mut s, chip, "vue"); assert!(vue.contains(" icon?: unknown;"), "{vue}"); + assert!(vue.contains(" icon: HeartIcon,"), "the swap defaults to its component: {vue}"); assert!(vue.contains(""), "a swap is a component, not an element: {vue}"); let i = idv(&ex(&mut s, "instance.create", json!({"component": chip, "x": 0, "y": 100}))); ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Icon": star}}));