diff --git a/slhx-build/src/lib.rs b/slhx-build/src/lib.rs index d1f8961..43ceda1 100644 --- a/slhx-build/src/lib.rs +++ b/slhx-build/src/lib.rs @@ -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("') 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("").unwrap_or(rest.len()); + let form_body = &rest[..form_body_end]; + if !__slhx_has_handle_input(form_body) { + out.push_str(&::std::format!("", 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#"
  • "#, + ) + .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(::std::marker::PhantomData); + impl Slot {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} + #[derive(Clone, Copy)] pub struct KeyedSlot(::std::marker::PhantomData<(K, T)>); + impl KeyedSlot {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} + #[derive(Clone, Copy)] pub struct Handle(::std::marker::PhantomData); + impl Handle {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} + #[derive(Clone, Copy)] pub struct Atom(::std::marker::PhantomData); + impl Atom {{ pub const fn new(_: u32) -> Self {{ Self(::std::marker::PhantomData) }} }} + #[derive(Clone, Copy)] pub struct Form(::std::marker::PhantomData); + impl Form {{ 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#"
  • "#); + 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())) }