Preserve native interaction semantics

This commit is contained in:
tmk241
2026-09-01 09:21:23 +02:00
parent 3323aaecd6
commit 53f0a5db1a
11 changed files with 135 additions and 115 deletions
+43 -44
View File
@@ -1029,17 +1029,17 @@ fn __hemx_lower_html(html: &str, table: &[(&str, &str, u32)]) -> ::std::string::
let mut out = html.to_owned();
for (attr, name, id) in table {
let runtime_attr = match *attr {
"data-hemx-slot" => "data-sid",
"data-hemx-handle" => "data-hid",
"data-hemx-form" => "data-fid",
"data-hemx-slot" => "data-hemx-resource",
"data-hemx-handle" => "data-hemx-action",
"data-hemx-form" => "data-hemx-resource",
"data-hemx-revealed" => "data-hemx-revealed",
"data-hemx-atom" => "data-aid",
"data-hemx-atom" => "data-hemx-resource",
_ => continue,
};
out = __hemx_replace_attr(out, attr, name, runtime_attr, *id);
}
out = __hemx_lower_static_attr(out, "data-hemx-key", "data-key");
out = __hemx_lower_static_attr(out, "h-key", "data-key");
out = __hemx_lower_static_attr(out, "data-hemx-key", "data-hemx-key");
out = __hemx_lower_static_attr(out, "h-key", "data-hemx-key");
__hemx_inject_handle_inputs(out)
}
@@ -1075,7 +1075,7 @@ fn __hemx_inject_handle_inputs(html: ::std::string::String) -> ::std::string::St
out.push_str(opening);
rest = &rest[open_end + 1..];
let Some(handle_id) = __hemx_attr(opening, "data-hid") else {
let Some(handle_id) = __hemx_attr(opening, "data-hemx-action") else {
continue;
};
let form_body_end = rest.find("</form>").unwrap_or(rest.len());
@@ -1536,7 +1536,6 @@ fn known_hemx_attr(name: &str) -> bool {
| "data-hemx-indicator"
| "data-hemx-confirm"
| "data-hemx-debounce"
| "data-hemx-delay"
| "data-hemx-throttle"
| "data-hemx-every"
| "data-hemx-interval"
@@ -1622,7 +1621,7 @@ fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
"expected a non-negative number of viewports",
));
}
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle" | "data-hemx-every"
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
| "data-hemx-interval"
if !valid_duration(value) =>
{
@@ -2025,7 +2024,7 @@ fn unkeyed_generated_target_diagnostic(
"a stable template h-key on h-for=\"{pattern} in {expr}\" so generated keyed helpers such as ui::{name}.replace(row) can target this partial"
),
repair: format!(
"add h-key=\"{pattern}.id\" to that h-for; dynamic +data-key on the child is rendered HTML, not the template fact hemx uses for generated targets"
"add h-key=\"{pattern}.id\" to that h-for; dynamic +data-hemx-key on the child is rendered HTML, not the template fact hemx uses for generated targets"
),
}
}
@@ -2655,12 +2654,12 @@ mod tests {
let canonical_syms = resources.syms();
assert_eq!(
stable_id("generated-rs", &canonical_generated),
262_445_078,
3_787_638_288,
"canonical generated Rust changed"
);
assert_eq!(
stable_id("generated-rs-global", &canonical_globals),
3_211_924_671,
1_112_484_789,
"canonical global-export Rust changed"
);
assert_eq!(
@@ -3036,8 +3035,9 @@ mod tests {
assert!(generated.contains("pub fn replace<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect"));
assert!(generated.contains("data-hemx-slot"));
assert!(generated.contains("data-hemx-form"));
assert!(generated.contains("data-sid"));
assert!(generated.contains("data-fid"));
assert!(generated.contains("data-hemx-resource"));
assert!(!generated.contains("data-sid"));
assert!(!generated.contains("data-fid"));
assert!(generated.contains("__hemx_inject_handle_inputs"));
let syms = std::fs::read_to_string(out.join("hemx.syms")).unwrap();
@@ -3224,7 +3224,7 @@ mod tests {
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todos.heml"),
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li +data-key="todo.id">{+ todo +}</li></template></ul>"#,
r#"<ul data-hemx-slot="row"><template h-for="todo in &self.todos" h-key="todo.id"><li +data-hemx-key="todo.id">{+ todo +}</li></template></ul>"#,
)
.unwrap();
@@ -3465,6 +3465,8 @@ mod hemx {{
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
pub struct Effect;
#[derive(Clone, Copy)] pub struct ResourceId;
#[derive(Clone, Copy)] pub struct EventName(&'static str);
impl EventName {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} }}
pub trait IntoEffect {{}}
impl IntoEffect for Effect {{}}
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
@@ -3722,7 +3724,9 @@ fn main() {{
.contains("h-for=\"todo in &self.todos\" without h-key"));
assert!(diagnostic.expected.contains("ui::todo_row.replace(row)"));
assert!(diagnostic.repair.contains("h-key=\"todo.id\""));
assert!(diagnostic.repair.contains("dynamic +data-key on the child"));
assert!(diagnostic
.repair
.contains("dynamic +data-hemx-key on the child"));
assert!(diagnostic.to_string().contains("templates/todo.heml"));
}
@@ -3737,7 +3741,7 @@ fn main() {{
),
(
"slot_data_key",
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-key="todo.id">{+ todo.title +}</li></template>"#,
r#"<template h-for="todo in &self.todos"><li data-hemx-slot="todo_row" +data-hemx-key="todo.id">{+ todo.title +}</li></template>"#,
"data-hemx-slot=\"todo_row\"",
"ui::todo_row",
),
@@ -3778,7 +3782,7 @@ fn main() {{
assert!(err.contains("h-key=\"todo.id\""));
assert!(err.contains("generated keyed helpers"));
assert!(err.contains(helper));
assert!(err.contains("dynamic +data-key on the child"));
assert!(err.contains("dynamic +data-hemx-key on the child"));
let _ = std::fs::remove_dir_all(&base);
}
@@ -3985,12 +3989,6 @@ fn main() {{
"data-hemx-ws",
"non-empty",
),
(
"delay-empty",
r#"<button data-hemx-handle="save" data-hemx-delay="">Save</button>"#,
"data-hemx-delay",
"milliseconds",
),
(
"revealed-ahead-negative",
r#"<form data-hemx-revealed data-hemx-revealed-ahead="-1"></form>"#,
@@ -4003,12 +4001,6 @@ fn main() {{
"data-hemx-throttle",
"milliseconds",
),
(
"duration-suffix-only",
r#"<button data-hemx-handle="save" data-hemx-delay="s">Save</button>"#,
"data-hemx-delay",
"milliseconds",
),
(
"policy-duplicate-anchor",
r#"<button data-hemx-handle="save" data-hemx-policy="newest">Save</button>"#,
@@ -4021,12 +4013,6 @@ fn main() {{
"data-hemx-debounce",
"250ms",
),
(
"delay",
r#"<button data-hemx-handle="save" data-hemx-delay="soon">Save</button>"#,
"data-hemx-delay",
"250ms",
),
(
"every",
r#"<button data-hemx-handle="save" data-hemx-every="1sec">Save</button>"#,
@@ -4103,8 +4089,8 @@ fn main() {{
"data-hemx-confirm" => "expected a non-empty confirmation message",
"data-hemx-sse" => "expected a non-empty same-origin SSE URL",
"data-hemx-ws" => "expected a non-empty same-origin WebSocket URL",
"data-hemx-debounce" | "data-hemx-delay" | "data-hemx-throttle"
| "data-hemx-every" | "data-hemx-interval" => {
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
| "data-hemx-interval" => {
"expected milliseconds like `250`/`250ms` or seconds like `1s`"
}
_ => panic!("missing expectation for {attr}"),
@@ -4170,7 +4156,7 @@ fn main() {{
let valid_source = r#"
<main data-hemx-root="app" data-hemx-sse="/events" data-hemx-ws="/room/socket">
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-delay="250ms" data-hemx-throttle="1s">Save</button>
<button data-hemx-handle="save" data-hemx-policy="latest" data-hemx-on="click change" data-hemx-confirm="Save?" data-hemx-debounce="250ms" data-hemx-throttle="1s">Save</button>
<form method="get" action="/search" data-hemx-history="replace"><input name="q"></form>
</main>
"#;
@@ -4282,7 +4268,7 @@ fn main() {{
}
#[test]
fn generated_lowering_injects_progressive_form_handle_and_key() {
fn generated_lowering_preserves_native_interaction_contract() {
let base = test_dir("hemx-build-lowering-test");
let templates = base.join("templates");
let out = base.join("out");
@@ -4290,7 +4276,7 @@ fn main() {{
std::fs::create_dir_all(&templates).unwrap();
std::fs::write(
templates.join("todo.heml"),
r#"<form data-hemx-handle="create"><input name="title"></form><li data-hemx-slot="row" data-hemx-key="7"></li>"#,
r#"<main data-hemx-root><a href="/docs">Docs</a><form action="/todos" method="post" data-hemx-handle="create"><label for="title">Title</label><input id="title" name="title" aria-invalid="true" aria-describedby="title-error"><span id="title-error">Required</span><button data-hemx-on="click">Create</button></form><form action="/search" method="get"><input name="q"></form><li data-hemx-slot="row" data-hemx-key="7"></li></main>"#,
)
.unwrap();
@@ -4305,6 +4291,8 @@ mod hemx {{
impl BuildFingerprint {{ pub const fn from_parts(_: &[u32]) -> Self {{ Self }} }}
pub struct Effect;
#[derive(Clone, Copy)] pub struct ResourceId;
#[derive(Clone, Copy)] pub struct EventName(&'static str);
impl EventName {{ pub const fn new(name: &'static str) -> Self {{ Self(name) }} }}
pub trait IntoEffect {{}}
impl IntoEffect for Effect {{}}
pub trait GeneratedTarget {{ fn __hemx_resource_id(self) -> ResourceId; }}
@@ -4337,10 +4325,21 @@ mod hemplate {{
}}
{generated}
fn main() {{
let html = todo::lower_html(r#"<form data-hemx-handle="create"><input name="title"></form><li data-hemx-slot="row" h-key="7"></li>"#);
assert!(html.contains(r#"data-hid="#));
let html = todo::lower_html(r#"<main data-hemx-root><a href="/docs">Docs</a><form action="/todos" method="post" data-hemx-handle="create"><label for="title">Title</label><input id="title" name="title" aria-invalid="true" aria-describedby="title-error"><span id="title-error">Required</span><button data-hemx-on="click">Create</button></form><form action="/search" method="get"><input name="q"></form><li data-hemx-slot="row" h-key="7"></li></main>"#);
assert!(html.contains(r#"data-hemx-root"#));
assert!(html.contains(r#"data-hemx-action="#));
assert!(html.contains(r#"data-hemx-resource="#));
assert!(html.contains(r#"data-hemx-key="7""#));
assert!(html.contains(r#"data-hemx-on="click""#));
assert!(html.contains(r#"<a href="/docs">"#));
assert!(html.contains(r#"action="/todos" method="post""#));
assert!(html.contains(r#"action="/search" method="get""#));
assert!(html.contains(r#"aria-invalid="true""#));
assert!(html.contains(r#"aria-describedby="title-error""#));
assert!(html.contains(r#"id="title-error">Required"#));
assert!(html.contains(r#"name="__h""#));
assert!(html.contains(r#"data-key="7""#));
assert!(!html.contains("data-hid"));
assert!(!html.contains("data-sid"));
assert!(!html.contains("h-key"));
}}
"###,