feat(build): inject lowered form handles

req: form/002

req: wire/001
This commit is contained in:
slhx agent
2026-05-10 23:35:46 +02:00
parent 79414fbb39
commit af813edd52
+115 -1
View File
@@ -332,7 +332,8 @@ fn __slhx_lower_html(html: &str, table: &[(&str, &str, u32)]) -> ::std::string::
};
out = __slhx_replace_attr(out, attr, name, runtime_attr, *id);
}
out
out = __slhx_lower_static_attr(out, "data-slhx-key", "data-key");
__slhx_inject_handle_inputs(out)
}
fn __slhx_replace_attr(mut html: ::std::string::String, attr: &str, name: &str, runtime_attr: &str, id: u32) -> ::std::string::String {
@@ -342,6 +343,55 @@ fn __slhx_replace_attr(mut html: ::std::string::String, attr: &str, name: &str,
let single = ::std::format!("{attr}='{name}'");
html.replace(&single, &replacement)
}
fn __slhx_lower_static_attr(mut html: ::std::string::String, attr: &str, runtime_attr: &str) -> ::std::string::String {
html = html.replace(&::std::format!("{attr}=\""), &::std::format!("{runtime_attr}=\""));
html.replace(&::std::format!("{attr}='"), &::std::format!("{runtime_attr}='"))
}
fn __slhx_inject_handle_inputs(html: ::std::string::String) -> ::std::string::String {
let mut out = ::std::string::String::with_capacity(html.len());
let mut rest = html.as_str();
while let Some(start) = rest.find("<form") {
out.push_str(&rest[..start]);
rest = &rest[start..];
let Some(open_end) = rest.find('>') else {
out.push_str(rest);
return out;
};
let opening = &rest[..=open_end];
out.push_str(opening);
rest = &rest[open_end + 1..];
let Some(handle_id) = __slhx_attr(opening, "data-hid") else {
continue;
};
let form_body_end = rest.find("</form>").unwrap_or(rest.len());
let form_body = &rest[..form_body_end];
if !__slhx_has_handle_input(form_body) {
out.push_str(&::std::format!("<input type=\"hidden\" name=\"__h\" value=\"{}\">", handle_id));
}
}
out.push_str(rest);
out
}
fn __slhx_has_handle_input(html: &str) -> bool {
html.contains("name=\"__h\"") || html.contains("name='__h'")
}
fn __slhx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
let attr_at = tag.find(attr)?;
let after_attr = &tag[attr_at + attr.len()..];
let after_equals = after_attr.trim_start().strip_prefix('=')?.trim_start();
let quote = after_equals.chars().next()?;
if quote != '\"' && quote != '\'' {
return None;
}
let value = &after_equals[quote.len_utf8()..];
let end = value.find(quote)?;
Some(value[..end].to_owned())
}
"#);
}
@@ -660,6 +710,7 @@ mod tests {
assert!(generated.contains("data-slhx-form"));
assert!(generated.contains("data-sid"));
assert!(generated.contains("data-fid"));
assert!(generated.contains("__slhx_inject_handle_inputs"));
let syms = std::fs::read_to_string(out.join("slhx.syms")).unwrap();
assert!(syms.contains("atom\t"));
@@ -688,6 +739,69 @@ mod tests {
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn generated_lowering_injects_progressive_form_handle_and_key() {
// req: form/002, req: wire/001
let base = test_dir("slhx-build-lowering-test");
let templates = base.join("templates");
let out = base.join("out");
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todo.heml"),
r#"<form data-slhx-handle="create"><input name="title"></form><li data-slhx-slot="row" data-slhx-key="7"></li>"#,
)
.unwrap();
app().template_dir(&templates).out_dir(&out).run().unwrap();
let generated = std::fs::read_to_string(out.join("slhx.generated.rs")).unwrap();
assert!(generated.contains("fn __slhx_lower_html"));
let source = format!(
r###"
#![allow(dead_code)]
mod slhx {{
#[derive(Clone, Copy)] pub struct BuildFingerprint;
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
#[derive(Clone, Copy)] pub struct Slot<T>(::std::marker::PhantomData<T>);
impl<T> Slot<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
#[derive(Clone, Copy)] pub struct KeyedSlot<K, T>(::std::marker::PhantomData<(K, T)>);
impl<K, T> KeyedSlot<K, T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
#[derive(Clone, Copy)] pub struct Handle<T>(::std::marker::PhantomData<T>);
impl<T> Handle<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
#[derive(Clone, Copy)] pub struct Atom<T>(::std::marker::PhantomData<T>);
impl<T> Atom<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
#[derive(Clone, Copy)] pub struct Form<T>(::std::marker::PhantomData<T>);
impl<T> Form<T> {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }}
pub struct FormContract {{ pub fields: &'static [FormField] }}
pub struct FormField {{ pub name: &'static str, pub kind: FormControlKind, pub required: bool }}
pub enum FormControlKind {{ Text, Number {{ min: Option<&'static str>, max: Option<&'static str>, step: Option<&'static str> }}, Checkbox, Radio, Select {{ multiple: bool }}, TextArea, File, Hidden, Submit, Other {{ tag: &'static str, input_type: Option<&'static str> }} }}
}}
{generated}
fn main() {{
let html = lower_html(r#"<form data-slhx-handle="create"><input name="title"></form><li data-slhx-slot="row" data-slhx-key="7"></li>"#);
assert!(html.contains(r#"data-hid="#));
assert!(html.contains(r#"name="__h""#));
assert!(html.contains(r#"data-key="7""#));
assert!(!html.contains("data-slhx-key"));
}}
"###,
);
let source_path = base.join("lowering.rs");
let bin_path = base.join("lowering-bin");
std::fs::write(&source_path, source).unwrap();
let status = std::process::Command::new("rustc")
.arg(&source_path)
.arg("-o")
.arg(&bin_path)
.status()
.unwrap();
assert!(status.success());
let status = std::process::Command::new(&bin_path).status().unwrap();
assert!(status.success());
let _ = std::fs::remove_dir_all(&base);
}
fn test_dir(prefix: &str) -> PathBuf {
std::env::temp_dir().join(format!("{prefix}-{}", std::process::id()))
}