mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 21:06:50 -06:00
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=""Buy""` 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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user