feat(build): generate event constants
Discover data-slhx-on event names from hemplate Surface inputs and emit generated events modules plus symbol rows without adding runtime trigger semantics. req: codegen/006
This commit is contained in:
@@ -237,6 +237,9 @@ a `data-*` handle param is statically known or runtime-extracted.
|
||||
### req: codegen/005
|
||||
005 Generated module `atoms` exports `Atom<T>` for values that must be addressable, bootstrapped, or synced. Ordinary Rust fields on app/components are not automatically atoms.
|
||||
|
||||
### req: codegen/006
|
||||
006 `slhx-build` discovers `data-slhx-on` event names from hemplate Surface inputs and emits generated event constants. Event constants are metadata for checked Rust authoring and diagnostics; they do not create a trigger mini-language or new browser runtime semantics. [north_star]
|
||||
|
||||
---
|
||||
|
||||
## public_api
|
||||
|
||||
+77
-1
@@ -76,6 +76,7 @@ struct Resources {
|
||||
forms: BTreeMap<String, FormResource>,
|
||||
atoms: BTreeMap<String, Resource>,
|
||||
classes: BTreeMap<String, ClassToken>,
|
||||
events: BTreeMap<String, EventToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -108,6 +109,14 @@ struct ClassToken {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct EventToken {
|
||||
symbol: String,
|
||||
ident: String,
|
||||
component: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Resources {
|
||||
fn add_surface(&mut self, root: &Path, path: &Path, surface: &SurfaceDocument) -> io::Result<()> {
|
||||
let component = component_ident(root, path)?;
|
||||
@@ -123,6 +132,13 @@ impl Resources {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(on_attr) = static_attr(&node.attrs, "data-slhx-on") {
|
||||
for event in event_tokens(&on_attr) {
|
||||
let canonical = canonical_symbol(root, path, event);
|
||||
self.insert_event(canonical, event.to_owned(), component.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = static_attr(&node.attrs, "data-slhx-slot") {
|
||||
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
|
||||
let keyed = is_inside_keyed_for(surface, node.scope);
|
||||
@@ -232,6 +248,27 @@ impl Resources {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_event(&mut self, symbol: String, name: String, component: String) -> io::Result<()> {
|
||||
let ident = event_ident(&name).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("invalid slhx event `{name}`; expected an ASCII event name usable from Rust"),
|
||||
)
|
||||
})?;
|
||||
let event = EventToken { symbol, ident, component, name };
|
||||
match self.events.get(&event.symbol) {
|
||||
Some(existing) if existing.name != event.name => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("conflicting event name for `{}`", existing.symbol),
|
||||
)),
|
||||
Some(_) => Ok(()),
|
||||
None => {
|
||||
self.events.insert(event.symbol.clone(), event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_form(
|
||||
&mut self,
|
||||
symbol: String,
|
||||
@@ -360,6 +397,20 @@ impl Resources {
|
||||
}
|
||||
out.push_str(&format!("{pad}}}\n\n"));
|
||||
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod events {{\n"));
|
||||
let mut emitted_events = BTreeSet::new();
|
||||
for event in self.events.values().filter(|event| event_matches(event, component)) {
|
||||
if !emitted_events.insert(event.ident.as_str()) {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&format!(
|
||||
"{inner}pub const {}: &'static str = {};\n",
|
||||
event.ident,
|
||||
rust_str(&event.name)
|
||||
));
|
||||
}
|
||||
out.push_str(&format!("{pad}}}\n\n"));
|
||||
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod forms {{\n"));
|
||||
for form in self.forms.values().filter(|form| component_matches(&form.resource, component)) {
|
||||
let res = &form.resource;
|
||||
@@ -515,6 +566,7 @@ fn __slhx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
|
||||
.chain(self.atoms.values().map(|res| &res.component))
|
||||
.chain(self.forms.values().map(|form| &form.resource.component))
|
||||
.chain(self.classes.values().map(|class| &class.component))
|
||||
.chain(self.events.values().map(|event| &event.component))
|
||||
{
|
||||
if !components.contains(component) {
|
||||
components.push(component.clone());
|
||||
@@ -550,6 +602,9 @@ fn __slhx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
|
||||
for class in self.classes.values() {
|
||||
out.push_str(&format!("class\t{}\t{}\t{}\n", class.symbol, class.ident, class.token));
|
||||
}
|
||||
for event in self.events.values() {
|
||||
out.push_str(&format!("event\t{}\t{}\t{}\n", event.symbol, event.ident, event.name));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -701,10 +756,21 @@ fn class_matches(class: &ClassToken, component: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn event_matches(event: &EventToken, component: Option<&str>) -> bool {
|
||||
match component {
|
||||
Some(component) => event.component == component,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn class_tokens(value: &str) -> impl Iterator<Item = &str> {
|
||||
value.split_ascii_whitespace().filter(|token| !token.is_empty())
|
||||
}
|
||||
|
||||
fn event_tokens(value: &str) -> impl Iterator<Item = &str> {
|
||||
value.split_ascii_whitespace().filter(|token| !token.is_empty())
|
||||
}
|
||||
|
||||
fn stylesheet_class_tokens(source: &str) -> Vec<&str> {
|
||||
let bytes = source.as_bytes();
|
||||
let mut tokens = Vec::new();
|
||||
@@ -787,6 +853,10 @@ fn data_param_ident(name: &str) -> Option<String> {
|
||||
rust_ident(&data_name.replace('-', "_"))
|
||||
}
|
||||
|
||||
fn event_ident(name: &str) -> Option<String> {
|
||||
rust_ident(&name.replace(['-', ':'], "_"))
|
||||
}
|
||||
|
||||
fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
|
||||
loop {
|
||||
let Some(current) = surface.scopes.get(scope.0 as usize) else {
|
||||
@@ -927,7 +997,7 @@ mod tests {
|
||||
std::fs::create_dir_all(&templates).unwrap();
|
||||
std::fs::write(
|
||||
templates.join("todo.heml"),
|
||||
r#"<form data-slhx-handle="create" data-slhx-form="new_todo"><input name="title"></form><button data-slhx-handle="delete" data-todo-id="7">Delete</button><ul data-slhx-slot="todos"></ul><section data-slhx-atom="filter"></section>"#,
|
||||
r#"<form data-slhx-handle="create" data-slhx-form="new_todo"><input name="title"></form><button data-slhx-handle="delete" data-todo-id="7" data-slhx-on="click keydown">Delete</button><ul data-slhx-slot="todos"></ul><section data-slhx-atom="filter"></section>"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -941,6 +1011,9 @@ mod tests {
|
||||
assert!(generated.contains("pub mod forms"));
|
||||
assert!(generated.contains("pub mod atoms"));
|
||||
assert!(generated.contains("pub const filter"));
|
||||
assert!(generated.contains("pub mod events"));
|
||||
assert!(generated.contains("pub const click: &'static str = \"click\""));
|
||||
assert!(generated.contains("pub const keydown: &'static str = \"keydown\""));
|
||||
assert!(generated.contains("pub const new_todo"));
|
||||
assert!(generated.contains("pub mod todo"));
|
||||
assert!(generated.contains("pub const ALL_IDS"));
|
||||
@@ -956,6 +1029,9 @@ mod tests {
|
||||
assert!(syms.contains("\tfilter\t"));
|
||||
assert!(syms.contains("handle_form\tcreate\tnew_todo\n"));
|
||||
assert!(syms.contains("handle_param\tdelete\ttodo_id\n"));
|
||||
assert!(syms.contains("event\t"));
|
||||
assert!(syms.contains("\tclick\tclick\n"));
|
||||
assert!(syms.contains("\tkeydown\tkeydown\n"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user