refactor(engine): simplify asset and code generation helpers

This commit is contained in:
Dylan M. Taylor
2026-10-28 16:00:36 +00:00
parent 5e16683963
commit d87f9effe1
36 changed files with 1062 additions and 849 deletions
+77 -57
View File
@@ -1,5 +1,5 @@
use super::css::{Tokens, css_decls};
use super::*;
use super::{CodeLang, Ctx, Document, LayoutMode, Node, NodeId, NodeKind, Param, Sizing, indent, num, with_v_if};
// ------------------------------------------------------------------ HTML and React markup
@@ -127,6 +127,32 @@ pub(crate) struct Markup<'c, 'a> {
pub fluid_leaves: f64,
}
/// How `n` sits in its parent: in the flex flow, or placed at its position.
fn placement_decls(n: &Node, parent: Option<&Node>, root: bool, in_flow: bool) -> String {
let mut decls = String::new();
if root {
decls.push_str("position: relative;\n");
} else if in_flow {
let vertical = parent.and_then(|p| p.layout.as_ref()).is_some_and(|l| l.mode == LayoutMode::Vertical);
let (main, cross) =
if vertical { (n.child_layout.sizing_v, n.child_layout.sizing_h) } else { (n.child_layout.sizing_h, n.child_layout.sizing_v) };
if main == Sizing::Fill {
decls.push_str("flex: 1 1 0;\n");
}
if cross == Sizing::Fill {
decls.push_str("align-self: stretch;\n");
}
} else {
let c = n.transform.as_coeffs();
decls.push_str(&format!("position: absolute;\nleft: {}px;\ntop: {}px;\n", num(c[4]), num(c[5])));
let rot = figcraft_geom::rotation_degrees(n.transform);
if rot.abs() > 0.01 {
decls.push_str(&format!("transform: rotate({}deg);\ntransform-origin: 0 0;\n", num(-rot)));
}
}
decls
}
impl<'c, 'a> Markup<'c, 'a> {
pub fn new(ctx: &'c Ctx<'a>, lang: CodeLang) -> Self {
Markup {
@@ -196,6 +222,52 @@ impl<'c, 'a> Markup<'c, 'a> {
self.rules.push((path.to_string(), class.to_string(), decls));
}
/// An instance as a call to its component (see `element`); true when it was written.
fn instance_call(&mut self, n: &Node, depth: usize, path: &str, class: &str, decls: &str, root: bool) -> bool {
let pad = self.pad(depth);
let attr = if self.lang == CodeLang::React { "className" } else { "class" };
let connected = self.inline && crate::code_connect::link(self.ctx.doc, n.id, CodeLang::React).is_some();
if self.code() && (!self.inline || connected) && !root && matches!(n.kind, NodeKind::Instance { .. }) {
let at = depth + usize::from(!decls.is_empty());
let run = if self.lang == CodeLang::Leptos { ".run()" } else { "" };
let (inner, css) = match self.ctx.param(n, "mainComponent") {
Some(p) if self.lang == CodeLang::Vue => (Some(format!("{}<component :is=\"{}\" />\n", self.pad(at), self.ident(p))), String::new()),
Some(p) => (Some(format!("{}{{{}{run}}}\n", self.pad(at), self.ident(p))), String::new()),
None => self.ctx.call(n, self.lang, at).map_or((None, String::new()), |(code, css)| (Some(code), css)),
};
self.styles.push_str(&css);
if let Some(inner) = inner {
// A Vue guard set by `node` rides on the call when there is no wrapper.
let guard = if self.lang == CodeLang::Vue { self.v_if.take() } else { None };
if decls.is_empty() {
self.markup.push_str(&with_v_if(&inner, guard.as_deref()));
} else {
let guard = guard.map(|g| format!(" v-if=\"{g}\"")).unwrap_or_default();
self.markup.push_str(&format!("{pad}<div {attr}=\"{class}\"{guard}>\n{inner}{pad}</div>\n"));
self.rule(path, class, decls.to_string());
}
return true;
}
}
false
}
fn text_body(&self, n: &Node, t: &figcraft_doc::TextData) -> String {
match (self.ctx.param(n, "characters").filter(|_| self.code()), self.lang) {
(Some(p), CodeLang::Leptos) => format!("{{{}.clone()}}", self.ident(p)),
(Some(p), CodeLang::Vue) => format!("{{{{ {} }}}}", self.ident(p)),
(Some(p), _) => format!("{{{}}}", p.ident),
(None, CodeLang::React) => jsx_text(&t.characters),
// `view!` takes text as string literals and void elements self-closed.
(None, CodeLang::Leptos) => {
t.characters.chars().take(10_000).collect::<String>().lines().map(|l| format!("{l:?}")).collect::<Vec<_>>().join("<br/>")
}
// A Vue template reads `{{ }}` as interpolation, so braces are escaped.
(None, CodeLang::Vue) => html_escape(&t.characters).replace('{', "&#123;").replace('}', "&#125;"),
(None, _) => html_escape(&t.characters),
}
}
fn element(&mut self, n: &Node, parent: Option<&Node>, depth: usize, path: &str) {
let doc = self.ctx.doc;
let root = n.id == self.ctx.root;
@@ -215,53 +287,13 @@ impl<'c, 'a> Markup<'c, 'a> {
let pad = self.pad(depth);
let attr = if self.lang == CodeLang::React { "className" } else { "class" };
// How it sits in its parent: in the flex flow, or placed at its position.
let mut decls = String::new();
let in_flow = parent.is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute;
if root {
decls.push_str("position: relative;\n");
} else if in_flow {
let vertical = parent.and_then(|p| p.layout.as_ref()).is_some_and(|l| l.mode == LayoutMode::Vertical);
let (main, cross) =
if vertical { (n.child_layout.sizing_v, n.child_layout.sizing_h) } else { (n.child_layout.sizing_h, n.child_layout.sizing_v) };
if main == Sizing::Fill {
decls.push_str("flex: 1 1 0;\n");
}
if cross == Sizing::Fill {
decls.push_str("align-self: stretch;\n");
}
} else {
let c = n.transform.as_coeffs();
decls.push_str(&format!("position: absolute;\nleft: {}px;\ntop: {}px;\n", num(c[4]), num(c[5])));
let rot = figcraft_geom::rotation_degrees(n.transform);
if rot.abs() > 0.01 {
decls.push_str(&format!("transform: rotate({}deg);\ntransform-origin: 0 0;\n", num(-rot)));
}
}
let mut decls = placement_decls(n, parent, root, in_flow);
// Nested instances are calls (component code), in a wrapper that places them when needed.
// Page code inlines instances, except those connected to code (codeConnect.set): their
// snippet, e.g. a design-system `<Button>`.
let connected = self.inline && crate::code_connect::link(doc, n.id, CodeLang::React).is_some();
if self.code() && (!self.inline || connected) && !root && matches!(n.kind, NodeKind::Instance { .. }) {
let at = depth + usize::from(!decls.is_empty());
let run = if self.lang == CodeLang::Leptos { ".run()" } else { "" };
let (inner, css) = match self.ctx.param(n, "mainComponent") {
Some(p) if self.lang == CodeLang::Vue => (Some(format!("{}<component :is=\"{}\" />\n", self.pad(at), self.ident(p))), String::new()),
Some(p) => (Some(format!("{}{{{}{run}}}\n", self.pad(at), self.ident(p))), String::new()),
None => self.ctx.call(n, self.lang, at).map_or((None, String::new()), |(code, css)| (Some(code), css)),
};
self.styles.push_str(&css);
if let Some(inner) = inner {
// A Vue guard set by `node` rides on the call when there is no wrapper.
let guard = if self.lang == CodeLang::Vue { self.v_if.take() } else { None };
if decls.is_empty() {
self.markup.push_str(&with_v_if(&inner, guard.as_deref()));
} else {
let guard = guard.map(|g| format!(" v-if=\"{g}\"")).unwrap_or_default();
self.markup.push_str(&format!("{pad}<div {attr}=\"{class}\"{guard}>\n{inner}{pad}</div>\n"));
self.rule(path, &class, decls);
}
return;
}
if self.instance_call(n, depth, path, &class, &decls, root) {
return;
}
let hugs = n.layout.is_some() || matches!(n.kind, NodeKind::Text(_));
let hug = (hugs && n.child_layout.sizing_h == Sizing::Hug, hugs && n.child_layout.sizing_v == Sizing::Hug);
@@ -286,19 +318,7 @@ impl<'c, 'a> Markup<'c, 'a> {
let open = |tag: &str| format!("{pad}<{tag} {attr}=\"{class}\"{attrs}>");
match &n.kind {
NodeKind::Text(t) => {
let body = match (self.ctx.param(n, "characters").filter(|_| self.code()), self.lang) {
(Some(p), CodeLang::Leptos) => format!("{{{}.clone()}}", self.ident(p)),
(Some(p), CodeLang::Vue) => format!("{{{{ {} }}}}", self.ident(p)),
(Some(p), _) => format!("{{{}}}", p.ident),
(None, CodeLang::React) => jsx_text(&t.characters),
// `view!` takes text as string literals and void elements self-closed.
(None, CodeLang::Leptos) => {
t.characters.chars().take(10_000).collect::<String>().lines().map(|l| format!("{l:?}")).collect::<Vec<_>>().join("<br/>")
}
// A Vue template reads `{{ }}` as interpolation, so braces are escaped.
(None, CodeLang::Vue) => html_escape(&t.characters).replace('{', "&#123;").replace('}', "&#125;"),
(None, _) => html_escape(&t.characters),
};
let body = self.text_body(n, t);
self.markup.push_str(&format!("{}{body}</p>\n", open("p")));
}
_ => {