Isolate template authoring validation
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn reject_selector_target_attrs(
|
||||
path: &Path,
|
||||
attrs: &[SurfaceAttribute],
|
||||
) -> io::Result<()> {
|
||||
for attr in attrs {
|
||||
let name = attr.name.as_str();
|
||||
if matches!(name, "data-hemx-target" | "data-hemx-select") {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: `{name}` is selector-style targeting; hemx uses generated resources instead. Add data-hemx-slot to the local element and return an effect for that generated slot.",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn reject_unknown_hemx_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||
for attr in attrs {
|
||||
let name = attr.name.as_str();
|
||||
if name.starts_with("data-hemx-") && !known_hemx_attr(name) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: unknown hemx attribute `{name}`; check the spelling or use a non-hemx data-* attribute for app-specific metadata",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn known_hemx_attr(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"data-hemx-root"
|
||||
| "data-hemx-sse"
|
||||
| "data-hemx-ws"
|
||||
| "data-hemx-st"
|
||||
| "data-hemx-handle"
|
||||
| "data-hemx-slot"
|
||||
| "data-hemx-form"
|
||||
| "data-hemx-atom"
|
||||
| "data-hemx-key"
|
||||
| "data-hemx-on"
|
||||
| "data-hemx-pending-class"
|
||||
| "data-hemx-indicator"
|
||||
| "data-hemx-confirm"
|
||||
| "data-hemx-debounce"
|
||||
| "data-hemx-throttle"
|
||||
| "data-hemx-every"
|
||||
| "data-hemx-interval"
|
||||
| "data-hemx-revealed"
|
||||
| "data-hemx-revealed-ahead"
|
||||
| "data-hemx-disable-while-pending"
|
||||
| "data-hemx-policy"
|
||||
| "data-hemx-nav"
|
||||
| "data-hemx-history"
|
||||
| "data-hemx-boost"
|
||||
| "data-hemx-error-for"
|
||||
| "data-hemx-error"
|
||||
| "data-hemx-island"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn reject_invalid_hemx_attr_values(
|
||||
path: &Path,
|
||||
attrs: &[SurfaceAttribute],
|
||||
) -> io::Result<()> {
|
||||
for attr in attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.origin == AttributeOrigin::Static)
|
||||
{
|
||||
let value = attr.value.as_deref().unwrap_or("");
|
||||
match attr.name.as_str() {
|
||||
"data-hemx-policy" if !valid_policy(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected one of `latest`, `queue`, `drop`, or `parallel`",
|
||||
));
|
||||
}
|
||||
"data-hemx-history" if !matches!(value.trim(), "" | "push" | "replace") => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected `push`, `replace`, or empty for the default push behavior",
|
||||
));
|
||||
}
|
||||
"data-hemx-on" if !valid_event_list(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
|
||||
));
|
||||
}
|
||||
"data-hemx-confirm" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty confirmation message",
|
||||
));
|
||||
}
|
||||
"data-hemx-sse" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty same-origin SSE URL",
|
||||
));
|
||||
}
|
||||
"data-hemx-ws" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty same-origin WebSocket URL",
|
||||
));
|
||||
}
|
||||
"data-hemx-revealed-ahead"
|
||||
if !value
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.is_some_and(|value| value.is_finite() && value >= 0.0) =>
|
||||
{
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-negative number of viewports",
|
||||
));
|
||||
}
|
||||
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
|
||||
| "data-hemx-interval"
|
||||
if !valid_duration(value) =>
|
||||
{
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected milliseconds like `250`/`250ms` or seconds like `1s`",
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn reject_invalid_hemx_attr_placement(
|
||||
path: &Path,
|
||||
tag: &str,
|
||||
attrs: &[SurfaceAttribute],
|
||||
) -> io::Result<()> {
|
||||
if has_attr(attrs, "data-hemx-nav")
|
||||
&& (tag != "a"
|
||||
|| !has_attr(attrs, "href")
|
||||
|| static_attr(attrs, "href").is_some_and(|href| href.trim().is_empty()))
|
||||
{
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-nav",
|
||||
"expected a real `<a href=...>` link so navigation works without JavaScript",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-boost") && matches!(tag, "a" | "form") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-boost",
|
||||
"expected a container around descendant links/forms; use `data-hemx-nav` on anchors or `data-hemx-handle` on forms",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-sse") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-sse",
|
||||
"expected placement on the same element as `data-hemx-root`",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-ws") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-ws",
|
||||
"expected placement on the same element as `data-hemx-root`",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_attr(attrs: &[SurfaceAttribute], name: &str) -> bool {
|
||||
attrs.iter().any(|attr| attr.name == name)
|
||||
}
|
||||
|
||||
fn invalid_hemx_placement(path: &Path, attr: &str, expectation: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: invalid {attr} placement; {expectation}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_policy(value: &str) -> bool {
|
||||
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 {
|
||||
matches!(
|
||||
value,
|
||||
"click" | "submit" | "input" | "change" | "keydown" | "dragstart" | "dragover" | "drop"
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_duration(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
let digits = value
|
||||
.strip_suffix("ms")
|
||||
.or_else(|| value.strip_suffix('s'))
|
||||
.unwrap_or(value);
|
||||
!digits.is_empty() && digits.as_bytes().iter().all(u8::is_ascii_digit)
|
||||
}
|
||||
|
||||
fn invalid_hemx_value(path: &Path, attr: &str, value: &str, expectation: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: invalid {attr} value `{value}`; {expectation}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn reject_unkeyed_loop(
|
||||
surface: &SurfaceDocument,
|
||||
scope: ScopeId,
|
||||
path: &Path,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
) -> io::Result<()> {
|
||||
if let Some(diagnostic) =
|
||||
unkeyed_generated_target_diagnostic_for_scope(surface, scope, path, kind, name)
|
||||
{
|
||||
Err(diagnostic.to_io_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_attribute_value_validators_cover_every_boundary() {
|
||||
for valid in ["latest", " queue ", "drop", "parallel"] {
|
||||
assert!(valid_policy(valid), "rejected policy {valid:?}");
|
||||
}
|
||||
for invalid in ["", "newest", "latest queue", "LATEST"] {
|
||||
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
|
||||
}
|
||||
|
||||
for valid in [
|
||||
"click",
|
||||
"submit input change keydown dragstart dragover drop",
|
||||
" click change ",
|
||||
] {
|
||||
assert!(valid_event_list(valid), "rejected event list {valid:?}");
|
||||
}
|
||||
for invalid in ["", " ", "blur", "click blur"] {
|
||||
assert!(
|
||||
!valid_event_list(invalid),
|
||||
"accepted event list {invalid:?}"
|
||||
);
|
||||
}
|
||||
for valid in [
|
||||
"click",
|
||||
"submit",
|
||||
"input",
|
||||
"change",
|
||||
"keydown",
|
||||
"dragstart",
|
||||
"dragover",
|
||||
"drop",
|
||||
] {
|
||||
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
|
||||
}
|
||||
for invalid in ["", "blur", "Click"] {
|
||||
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
|
||||
}
|
||||
|
||||
for valid in ["0", "250", "250ms", "1s", " 5s "] {
|
||||
assert!(valid_duration(valid), "rejected duration {valid:?}");
|
||||
}
|
||||
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
|
||||
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-300
@@ -10,6 +10,13 @@ use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod authoring;
|
||||
|
||||
use authoring::{
|
||||
reject_invalid_hemx_attr_placement, reject_invalid_hemx_attr_values,
|
||||
reject_selector_target_attrs, reject_unkeyed_loop, reject_unknown_hemx_attrs,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum DiagnosticSeverity {
|
||||
Error,
|
||||
@@ -1487,262 +1494,6 @@ fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_selector_target_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||
for attr in attrs {
|
||||
let name = attr.name.as_str();
|
||||
if matches!(name, "data-hemx-target" | "data-hemx-select") {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: `{name}` is selector-style targeting; hemx uses generated resources instead. Add data-hemx-slot to the local element and return an effect for that generated slot.",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_unknown_hemx_attrs(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||
for attr in attrs {
|
||||
let name = attr.name.as_str();
|
||||
if name.starts_with("data-hemx-") && !known_hemx_attr(name) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: unknown hemx attribute `{name}`; check the spelling or use a non-hemx data-* attribute for app-specific metadata",
|
||||
path.display()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn known_hemx_attr(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"data-hemx-root"
|
||||
| "data-hemx-sse"
|
||||
| "data-hemx-ws"
|
||||
| "data-hemx-st"
|
||||
| "data-hemx-handle"
|
||||
| "data-hemx-slot"
|
||||
| "data-hemx-form"
|
||||
| "data-hemx-atom"
|
||||
| "data-hemx-key"
|
||||
| "data-hemx-on"
|
||||
| "data-hemx-pending-class"
|
||||
| "data-hemx-indicator"
|
||||
| "data-hemx-confirm"
|
||||
| "data-hemx-debounce"
|
||||
| "data-hemx-throttle"
|
||||
| "data-hemx-every"
|
||||
| "data-hemx-interval"
|
||||
| "data-hemx-revealed"
|
||||
| "data-hemx-revealed-ahead"
|
||||
| "data-hemx-disable-while-pending"
|
||||
| "data-hemx-policy"
|
||||
| "data-hemx-nav"
|
||||
| "data-hemx-history"
|
||||
| "data-hemx-boost"
|
||||
| "data-hemx-error-for"
|
||||
| "data-hemx-error"
|
||||
| "data-hemx-island"
|
||||
)
|
||||
}
|
||||
|
||||
fn reject_invalid_hemx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> io::Result<()> {
|
||||
for attr in attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.origin == AttributeOrigin::Static)
|
||||
{
|
||||
let value = attr.value.as_deref().unwrap_or("");
|
||||
match attr.name.as_str() {
|
||||
"data-hemx-policy" if !valid_policy(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected one of `latest`, `queue`, `drop`, or `parallel`",
|
||||
));
|
||||
}
|
||||
"data-hemx-history" if !matches!(value.trim(), "" | "push" | "replace") => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected `push`, `replace`, or empty for the default push behavior",
|
||||
));
|
||||
}
|
||||
"data-hemx-on" if !valid_event_list(value) => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected runtime-supported events: `click`, `submit`, `input`, `change`, `keydown`, `dragstart`, `dragover`, or `drop`",
|
||||
));
|
||||
}
|
||||
"data-hemx-confirm" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty confirmation message",
|
||||
));
|
||||
}
|
||||
"data-hemx-sse" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty same-origin SSE URL",
|
||||
));
|
||||
}
|
||||
"data-hemx-ws" if value.trim().is_empty() => {
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-empty same-origin WebSocket URL",
|
||||
));
|
||||
}
|
||||
"data-hemx-revealed-ahead"
|
||||
if !value
|
||||
.trim()
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.is_some_and(|value| value.is_finite() && value >= 0.0) =>
|
||||
{
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected a non-negative number of viewports",
|
||||
));
|
||||
}
|
||||
"data-hemx-debounce" | "data-hemx-throttle" | "data-hemx-every"
|
||||
| "data-hemx-interval"
|
||||
if !valid_duration(value) =>
|
||||
{
|
||||
return Err(invalid_hemx_value(
|
||||
path,
|
||||
&attr.name,
|
||||
value,
|
||||
"expected milliseconds like `250`/`250ms` or seconds like `1s`",
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reject_invalid_hemx_attr_placement(
|
||||
path: &Path,
|
||||
tag: &str,
|
||||
attrs: &[SurfaceAttribute],
|
||||
) -> io::Result<()> {
|
||||
if has_attr(attrs, "data-hemx-nav")
|
||||
&& (tag != "a"
|
||||
|| !has_attr(attrs, "href")
|
||||
|| static_attr(attrs, "href").is_some_and(|href| href.trim().is_empty()))
|
||||
{
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-nav",
|
||||
"expected a real `<a href=...>` link so navigation works without JavaScript",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-boost") && matches!(tag, "a" | "form") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-boost",
|
||||
"expected a container around descendant links/forms; use `data-hemx-nav` on anchors or `data-hemx-handle` on forms",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-sse") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-sse",
|
||||
"expected placement on the same element as `data-hemx-root`",
|
||||
));
|
||||
}
|
||||
if has_attr(attrs, "data-hemx-ws") && !has_attr(attrs, "data-hemx-root") {
|
||||
return Err(invalid_hemx_placement(
|
||||
path,
|
||||
"data-hemx-ws",
|
||||
"expected placement on the same element as `data-hemx-root`",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_attr(attrs: &[SurfaceAttribute], name: &str) -> bool {
|
||||
attrs.iter().any(|attr| attr.name == name)
|
||||
}
|
||||
|
||||
fn invalid_hemx_placement(path: &Path, attr: &str, expectation: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: invalid {attr} placement; {expectation}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_policy(value: &str) -> bool {
|
||||
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 {
|
||||
matches!(
|
||||
value,
|
||||
"click" | "submit" | "input" | "change" | "keydown" | "dragstart" | "dragover" | "drop"
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_duration(value: &str) -> bool {
|
||||
let value = value.trim();
|
||||
let digits = value
|
||||
.strip_suffix("ms")
|
||||
.or_else(|| value.strip_suffix('s'))
|
||||
.unwrap_or(value);
|
||||
!digits.is_empty() && digits.as_bytes().iter().all(u8::is_ascii_digit)
|
||||
}
|
||||
|
||||
fn invalid_hemx_value(path: &Path, attr: &str, value: &str, expectation: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"{}: invalid {attr} value `{value}`; {expectation}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn reject_unkeyed_loop(
|
||||
surface: &SurfaceDocument,
|
||||
scope: ScopeId,
|
||||
path: &Path,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
) -> io::Result<()> {
|
||||
if let Some(diagnostic) =
|
||||
unkeyed_generated_target_diagnostic_for_scope(surface, scope, path, kind, name)
|
||||
{
|
||||
Err(diagnostic.to_io_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn context_type_for_heml_path(path: &Path) -> Option<String> {
|
||||
let stem = path.file_stem()?.to_str()?;
|
||||
let mut out = String::new();
|
||||
@@ -4110,50 +3861,7 @@ fn main() {{
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_attribute_value_validators_cover_every_boundary() {
|
||||
for valid in ["latest", " queue ", "drop", "parallel"] {
|
||||
assert!(valid_policy(valid), "rejected policy {valid:?}");
|
||||
}
|
||||
for invalid in ["", "newest", "latest queue", "LATEST"] {
|
||||
assert!(!valid_policy(invalid), "accepted policy {invalid:?}");
|
||||
}
|
||||
|
||||
for valid in [
|
||||
"click",
|
||||
"submit input change keydown dragstart dragover drop",
|
||||
" click change ",
|
||||
] {
|
||||
assert!(valid_event_list(valid), "rejected event list {valid:?}");
|
||||
}
|
||||
for invalid in ["", " ", "blur", "click blur"] {
|
||||
assert!(
|
||||
!valid_event_list(invalid),
|
||||
"accepted event list {invalid:?}"
|
||||
);
|
||||
}
|
||||
for valid in [
|
||||
"click",
|
||||
"submit",
|
||||
"input",
|
||||
"change",
|
||||
"keydown",
|
||||
"dragstart",
|
||||
"dragover",
|
||||
"drop",
|
||||
] {
|
||||
assert!(valid_runtime_event(valid), "rejected event {valid:?}");
|
||||
}
|
||||
for invalid in ["", "blur", "Click"] {
|
||||
assert!(!valid_runtime_event(invalid), "accepted event {invalid:?}");
|
||||
}
|
||||
|
||||
for valid in ["0", "250", "250ms", "1s", " 5s "] {
|
||||
assert!(valid_duration(valid), "rejected duration {valid:?}");
|
||||
}
|
||||
for invalid in ["", " ", "ms", "s", "-1", "1sec", "1.5s", "1 ms"] {
|
||||
assert!(!valid_duration(invalid), "accepted duration {invalid:?}");
|
||||
}
|
||||
|
||||
fn static_attribute_values_integrate_with_template_inspection() {
|
||||
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-debounce="250ms" data-hemx-throttle="1s">Save</button>
|
||||
|
||||
Reference in New Issue
Block a user