fix(build): reject empty slhx guards

Treat empty static data-slhx-on lists and empty data-slhx-confirm messages as build errors, so authors do not silently disable dispatch or confirmation guards with blank attributes.

req: convention/002

req: convention/004

req: diagnostics/002
This commit is contained in:
slhx agent
2026-06-01 21:01:29 +02:00
parent b98820e819
commit 0315044f53
2 changed files with 29 additions and 6 deletions
+1 -1
View File
@@ -870,7 +870,7 @@ All forms support returning `impl IntoEffect` and compose through tuples.
003 `data-slhx-debounce` and `data-slhx-throttle` support simple millisecond values. No trigger mini-language in core. 003 `data-slhx-debounce` and `data-slhx-throttle` support simple millisecond values. No trigger mini-language in core.
### req: convention/004 ### req: convention/004
004 `data-slhx-confirm` dispatches a native `confirm()` before handler dispatch. Custom confirm UI belongs to integration crates. 004 `data-slhx-confirm` dispatches a native `confirm()` before handler dispatch. Static empty confirmation messages are build errors because they silently disable the guard in browsers. Custom confirm UI belongs to integration crates.
### req: convention/005 ### req: convention/005
005 `data-slhx-every` dispatches a handle at a fixed interval while the element remains in the document. Duplicate timers per root are avoided. 005 `data-slhx-every` dispatches a handle at a fixed interval while the element remains in the document. Duplicate timers per root are avoided.
+28 -5
View File
@@ -1015,9 +1015,7 @@ fn known_slhx_attr(name: &str) -> bool {
fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> { fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
for attr in attrs.iter().filter(|attr| attr.origin == AttributeOrigin::Static) { for attr in attrs.iter().filter(|attr| attr.origin == AttributeOrigin::Static) {
let Some(value) = attr.value.as_deref() else { let value = attr.value.as_deref().unwrap_or("");
continue;
};
match attr.name.as_str() { match attr.name.as_str() {
"data-slhx-policy" if !valid_policy(value) => { "data-slhx-policy" if !valid_policy(value) => {
return Err(invalid_slhx_value( return Err(invalid_slhx_value(
@@ -1027,7 +1025,7 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
"expected one of `latest`, `queue`, `drop`, or `parallel`", "expected one of `latest`, `queue`, `drop`, or `parallel`",
)); ));
} }
"data-slhx-on" if !event_tokens(value).all(valid_runtime_event) => { "data-slhx-on" if !valid_event_list(value) => {
return Err(invalid_slhx_value( return Err(invalid_slhx_value(
path, path,
&attr.name, &attr.name,
@@ -1035,6 +1033,14 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, or `drop`", "expected runtime-supported events: `click`, `submit`, `input`, `change`, `dragstart`, `dragover`, or `drop`",
)); ));
} }
"data-slhx-confirm" if value.trim().is_empty() => {
return Err(invalid_slhx_value(
path,
&attr.name,
value,
"expected a non-empty confirmation message",
));
}
"data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every" "data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every"
if !valid_duration(value) => if !valid_duration(value) =>
{ {
@@ -1055,6 +1061,11 @@ fn valid_policy(value: &str) -> bool {
matches!(value.trim(), "latest" | "queue" | "drop" | "parallel") matches!(value.trim(), "latest" | "queue" | "drop" | "parallel")
} }
fn valid_event_list(value: &str) -> bool {
let mut events = event_tokens(value).peekable();
events.peek().is_some() && events.all(valid_runtime_event)
}
fn valid_runtime_event(value: &str) -> bool { fn valid_runtime_event(value: &str) -> bool {
matches!(value, "click" | "submit" | "input" | "change" | "dragstart" | "dragover" | "drop") matches!(value, "click" | "submit" | "input" | "change" | "dragstart" | "dragover" | "drop")
} }
@@ -1529,7 +1540,7 @@ fn main() {{
#[test] #[test]
fn rejects_invalid_static_slhx_convention_values() { fn rejects_invalid_static_slhx_convention_values() {
// req: convention/003 req: convention/005 req: convention/006 req: diagnostics/002 // req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: diagnostics/002
for (case, template, attr, guidance) in [ for (case, template, attr, guidance) in [
( (
"policy", "policy",
@@ -1555,6 +1566,18 @@ fn main() {{
"data-slhx-on", "data-slhx-on",
"click", "click",
), ),
(
"empty-event",
r#"<button data-slhx-handle="save" data-slhx-on="">Save</button>"#,
"data-slhx-on",
"click",
),
(
"empty-confirm",
r#"<button data-slhx-handle="delete" data-slhx-confirm="">Delete</button>"#,
"data-slhx-confirm",
"non-empty",
),
] { ] {
let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test")); let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test"));
let templates = base.join("templates"); let templates = base.join("templates");