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.
This commit is contained in:
phonedu
2026-10-23 10:48:21 +08:00
parent 878e299724
commit c79bc04a29
4 changed files with 34 additions and 4 deletions
+28
View File
@@ -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<char>)> {
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),
+4
View File
@@ -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": "<Card title=\"{title} {title}\" />"}));
assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n<Card title=\"Say &quot;hi&quot; Say &quot;hi&quot;\" />\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": "<p>Total = {title}</p>"}));
assert_eq!(code(&mut s, i, "vue"), "import Card from \"./Card.vue\";\n\n<p>Total = Buy</p>\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");
+1 -4
View File
@@ -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</script>\n\n");
@@ -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("<component :is=\"icon\" />"), "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}}));