M6: fix Vue bindings, instance guards and slot catalogs

Vue connected templates keep a string's JS quotes inside bound attributes, so `:title={title}` becomes `:title="&quot;Buy&quot;"` rather than a bare identifier; static attributes take plain text. Nested instances no longer lose a boolean `v-if` guard, instance swaps render through `<component :is>`, slot names stay distinct when their idents lowercase alike, slot content CSS reaches the parent stylesheet, and text defaults escape `</script>` in the script block. The five locale catalogs the Vue key rename missed (bn, fi, ka, nl, pl) are updated, and the docs list Leptos/Vue consistently.
This commit is contained in:
phonedu
2026-10-22 19:52:58 +08:00
parent 7c211d8b0d
commit a132eb3768
14 changed files with 252 additions and 53 deletions
+56 -16
View File
@@ -447,6 +447,12 @@ pub(crate) fn js_string(s: &str) -> String {
serde_json::to_string(&s).unwrap_or_else(|_| "\"\"".into())
}
/// A JavaScript string literal safe inside a Vue single-file component: `<` is escaped, so a
/// default containing `</script>` can't close the `<script setup>` block early.
pub(crate) fn vue_string(s: &str) -> String {
js_string(s).replace('<', "\\u003C")
}
/// The type name of a component (a variant's set) by id.
pub(crate) fn component_type(doc: &Document, comp: NodeId) -> Option<String> {
let n = doc.node(comp).filter(|n| matches!(n.kind, NodeKind::Component { .. } | NodeKind::ComponentSet { .. }))?;
@@ -464,7 +470,8 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
let js = matches!(lang, CodeLang::React | CodeLang::Vue);
match (&p.kind, lang) {
(ParamKind::Boolean, _) => value.as_bool().unwrap_or(false).to_string(),
(ParamKind::Text, CodeLang::React | CodeLang::Vue) => js_string(value.as_str().unwrap_or("")),
(ParamKind::Text, CodeLang::Vue) => vue_string(value.as_str().unwrap_or("")),
(ParamKind::Text, CodeLang::React) => js_string(value.as_str().unwrap_or("")),
(ParamKind::Text, CodeLang::Leptos) => rust_string(value.as_str().unwrap_or("")),
(ParamKind::Variant(opts), CodeLang::Leptos) => {
let i = opts.iter().position(|o| Some(o.as_str()) == value.as_str()).unwrap_or(0);
@@ -473,7 +480,10 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
(ParamKind::Text, l) => literal(value.as_str().unwrap_or(""), l == CodeLang::Compose),
(ParamKind::Variant(opts), l) => {
let v = value.as_str().unwrap_or("");
if js {
if l == CodeLang::Vue {
return vue_string(v);
}
if l == CodeLang::React {
return js_string(v);
}
let swift = l == CodeLang::SwiftUi;
@@ -484,6 +494,8 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
(ParamKind::InstanceSwap, l) => {
let t = id_of(value).and_then(|i| component_type(doc, i));
match (t, l) {
// A Vue binding takes the component itself, not an element.
(Some(t), CodeLang::Vue) => t,
(Some(t), _) if js => format!("<{t} />"),
(Some(t), CodeLang::Leptos) => format!("|| view! {{ <{} /> }}", rust_type(&t)),
(None, CodeLang::Leptos) => "|| ()".into(),
@@ -500,6 +512,19 @@ pub(crate) fn value_expr(doc: &Document, c: &Component, p: &Param, value: &Value
// ------------------------------------------------------------------ instances
/// The `<slot name="…">` of a slot parameter: unique among the component's slot parameters, unlike
/// `class_name` called with a fresh list per parameter (two idents can lowercase alike).
pub(crate) fn slot_name(c: &Component, p: &Param) -> String {
let mut used = Vec::new();
for q in c.params.iter().filter(|q| matches!(q.kind, ParamKind::Slot)) {
let name = class_name(&q.ident, &mut used);
if q.ident == p.ident {
return name;
}
}
class_name(&p.ident, &mut Vec::new())
}
/// The values an instance sets for `c`'s parameters, in order (`None` for other layers): variant
/// values from the variant it shows, slots as whether they hold its own content.
pub(crate) fn instance_values(doc: &Document, id: NodeId, c: &Component) -> Option<Vec<(String, Value)>> {
@@ -524,23 +549,32 @@ pub(crate) fn instance_values(doc: &Document, id: NodeId, c: &Component) -> Opti
/// The call for instance `id` at `depth`, passing the values that differ from the defaults and the
/// content of slots it fills (`None` when it isn't an instance of an existing component).
pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option<String> {
usage_with_styles(doc, id, lang, depth).map(|(code, _)| code)
}
/// `usage` and the CSS generated for the slot content it carries (the caller's stylesheet needs
/// those rules; `usage` alone drops them at a snippet boundary).
pub(crate) fn usage_with_styles(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option<(String, String)> {
let c = main_of(doc, id).and_then(|m| describe(doc, m))?;
let values = instance_values(doc, id, &c)?;
let pad = if matches!(lang, CodeLang::React | CodeLang::Vue) { " ".repeat(depth) } else { indent(depth) };
// A connected component: its real snippet. shortcut: inside a layer tree it brings no import
// line; collect them at the top of the tree's code when people ask for it.
if let Some(link) = crate::code_connect::link(doc, id, lang) {
return Some(crate::code_connect::render(doc, &c, &values, link, lang, &pad));
return Some((crate::code_connect::render(doc, &c, &values, link, lang, &pad), String::new()));
}
let slots = slots_of(doc, id);
// (parameter, expression, whether it's a plain string literal)
let mut args: Vec<(&str, String, bool)> = Vec::new();
let mut slot_css = String::new();
for (p, (_, v)) in c.params.iter().zip(&values) {
if let ParamKind::Slot = p.kind {
if v.as_bool() == Some(true)
&& let Some((layer, _)) = slots.iter().find(|(_, name)| *name == p.prop)
{
args.push((&p.ident, slot_content(doc, *layer, lang, depth), false));
let (content, css) = slot_content(doc, *layer, lang, depth);
slot_css.push_str(&css);
args.push((&p.ident, content, false));
}
continue;
}
@@ -552,7 +586,7 @@ pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option
args.push((&p.ident, expr, plain));
}
let t = &c.type_name;
Some(match lang {
let code = match lang {
CodeLang::React | CodeLang::Leptos => {
let leptos = lang == CodeLang::Leptos;
// Plain strings read best as attribute values.
@@ -579,18 +613,24 @@ pub fn usage(doc: &Document, id: NodeId, lang: CodeLang, depth: usize) -> Option
let slots: String = args
.iter()
.filter(|(name, _, _)| is_slot(name))
.map(|(name, expr, _)| format!("{pad} <template #{}>\n{expr}{pad} </template>\n", class_name(name, &mut Vec::new())))
.map(|(name, expr, _)| {
let slot = c.params.iter().find(|q| q.ident == *name);
let name = slot.map(|p| slot_name(&c, p)).unwrap_or_else(|| class_name(name, &mut Vec::new()));
format!("{pad} <template #{name}>\n{expr}{pad} </template>\n")
})
.collect();
if slots.is_empty() { format!("{pad}<{t}{attrs} />\n") } else { format!("{pad}<{t}{attrs}>\n{slots}{pad}</{t}>\n") }
}
CodeLang::SwiftUi => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n}: {e}")).collect::<Vec<_>>().join(", ")),
_ => format!("{pad}{t}({})\n", args.iter().map(|(n, e, _)| format!("{n} = {e}")).collect::<Vec<_>>().join(", ")),
})
};
Some((code, slot_css))
}
/// The content an instance put in one of its slots, as an expression spanning lines.
fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize) -> String {
let Some(n) = doc.node(slot) else { return String::new() };
/// The content an instance put in one of its slots, as an expression spanning lines, and the CSS
/// its layers generated (so the caller's stylesheet keeps it).
fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize) -> (String, String) {
let Some(n) = doc.node(slot) else { return (String::new(), String::new()) };
let ctx = Ctx { doc, root: slot, comp: None, base: depth };
let kids = ctx.kids(n);
match lang {
@@ -600,14 +640,14 @@ fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize) -> S
m.node(k, Some(n), depth + 2, "");
}
let (p1, p2) = (" ".repeat(depth), " ".repeat(depth + 1));
format!("(\n{p2}<>\n{}{p2}</>\n{p1})", m.markup)
(format!("(\n{p2}<>\n{}{p2}</>\n{p1})", m.markup), m.styles)
}
CodeLang::Leptos => {
let mut m = Markup::new(&ctx, CodeLang::Leptos);
for k in kids {
m.node(k, Some(n), depth + 1, "");
}
format!("|| view! {{\n{}{}}}", m.markup, indent(depth))
(format!("|| view! {{\n{}{}}}", m.markup, indent(depth)), m.styles)
}
CodeLang::Vue => {
// Just the markup: `usage` wraps it in the `<template #name>` of that slot.
@@ -615,21 +655,21 @@ fn slot_content(doc: &Document, slot: NodeId, lang: CodeLang, depth: usize) -> S
for k in kids {
m.node(k, Some(n), depth + 2, "");
}
m.markup
(m.markup, m.styles)
}
CodeLang::SwiftUi => {
let mut out = String::new();
for k in kids {
swift(&ctx, k, depth + 2, &mut out, &[]);
}
format!("AnyView(Group {{\n{out}{}}})", indent(depth + 1))
(format!("AnyView(Group {{\n{out}{}}})", indent(depth + 1)), String::new())
}
_ => {
let mut out = String::new();
for k in kids {
compose(&ctx, k, depth + 2, &mut out);
}
format!("{{\n{out}{}}}", indent(depth + 1))
(format!("{{\n{out}{}}}", indent(depth + 1)), String::new())
}
}
}
@@ -752,7 +792,7 @@ fn vue(doc: &Document, c: &Component) -> String {
out.push_str("<script setup lang=\"ts\">\n\nconst props = withDefaults(\n defineProps<{\n");
for p in &props {
let ty = match &p.kind {
ParamKind::Variant(opts) => opts.iter().map(|o| js_string(o)).collect::<Vec<_>>().join(" | "),
ParamKind::Variant(opts) => opts.iter().map(|o| vue_string(o)).collect::<Vec<_>>().join(" | "),
ParamKind::Boolean => "boolean".into(),
ParamKind::Text => "string".into(),
ParamKind::InstanceSwap | ParamKind::Slot => "unknown".into(),