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
|
### 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.
|
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
|
## public_api
|
||||||
|
|||||||
+77
-1
@@ -76,6 +76,7 @@ struct Resources {
|
|||||||
forms: BTreeMap<String, FormResource>,
|
forms: BTreeMap<String, FormResource>,
|
||||||
atoms: BTreeMap<String, Resource>,
|
atoms: BTreeMap<String, Resource>,
|
||||||
classes: BTreeMap<String, ClassToken>,
|
classes: BTreeMap<String, ClassToken>,
|
||||||
|
events: BTreeMap<String, EventToken>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -108,6 +109,14 @@ struct ClassToken {
|
|||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct EventToken {
|
||||||
|
symbol: String,
|
||||||
|
ident: String,
|
||||||
|
component: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl Resources {
|
impl Resources {
|
||||||
fn add_surface(&mut self, root: &Path, path: &Path, surface: &SurfaceDocument) -> io::Result<()> {
|
fn add_surface(&mut self, root: &Path, path: &Path, surface: &SurfaceDocument) -> io::Result<()> {
|
||||||
let component = component_ident(root, path)?;
|
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") {
|
if let Some(name) = static_attr(&node.attrs, "data-slhx-slot") {
|
||||||
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
|
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
|
||||||
let keyed = is_inside_keyed_for(surface, node.scope);
|
let keyed = is_inside_keyed_for(surface, node.scope);
|
||||||
@@ -232,6 +248,27 @@ impl Resources {
|
|||||||
Ok(())
|
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(
|
fn insert_form(
|
||||||
&mut self,
|
&mut self,
|
||||||
symbol: String,
|
symbol: String,
|
||||||
@@ -360,6 +397,20 @@ impl Resources {
|
|||||||
}
|
}
|
||||||
out.push_str(&format!("{pad}}}\n\n"));
|
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"));
|
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)) {
|
for form in self.forms.values().filter(|form| component_matches(&form.resource, component)) {
|
||||||
let res = &form.resource;
|
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.atoms.values().map(|res| &res.component))
|
||||||
.chain(self.forms.values().map(|form| &form.resource.component))
|
.chain(self.forms.values().map(|form| &form.resource.component))
|
||||||
.chain(self.classes.values().map(|class| &class.component))
|
.chain(self.classes.values().map(|class| &class.component))
|
||||||
|
.chain(self.events.values().map(|event| &event.component))
|
||||||
{
|
{
|
||||||
if !components.contains(component) {
|
if !components.contains(component) {
|
||||||
components.push(component.clone());
|
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() {
|
for class in self.classes.values() {
|
||||||
out.push_str(&format!("class\t{}\t{}\t{}\n", class.symbol, class.ident, class.token));
|
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
|
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> {
|
fn class_tokens(value: &str) -> impl Iterator<Item = &str> {
|
||||||
value.split_ascii_whitespace().filter(|token| !token.is_empty())
|
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> {
|
fn stylesheet_class_tokens(source: &str) -> Vec<&str> {
|
||||||
let bytes = source.as_bytes();
|
let bytes = source.as_bytes();
|
||||||
let mut tokens = Vec::new();
|
let mut tokens = Vec::new();
|
||||||
@@ -787,6 +853,10 @@ fn data_param_ident(name: &str) -> Option<String> {
|
|||||||
rust_ident(&data_name.replace('-', "_"))
|
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 {
|
fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
|
||||||
loop {
|
loop {
|
||||||
let Some(current) = surface.scopes.get(scope.0 as usize) else {
|
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::create_dir_all(&templates).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
templates.join("todo.heml"),
|
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();
|
.unwrap();
|
||||||
|
|
||||||
@@ -941,6 +1011,9 @@ mod tests {
|
|||||||
assert!(generated.contains("pub mod forms"));
|
assert!(generated.contains("pub mod forms"));
|
||||||
assert!(generated.contains("pub mod atoms"));
|
assert!(generated.contains("pub mod atoms"));
|
||||||
assert!(generated.contains("pub const filter"));
|
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 const new_todo"));
|
||||||
assert!(generated.contains("pub mod todo"));
|
assert!(generated.contains("pub mod todo"));
|
||||||
assert!(generated.contains("pub const ALL_IDS"));
|
assert!(generated.contains("pub const ALL_IDS"));
|
||||||
@@ -956,6 +1029,9 @@ mod tests {
|
|||||||
assert!(syms.contains("\tfilter\t"));
|
assert!(syms.contains("\tfilter\t"));
|
||||||
assert!(syms.contains("handle_form\tcreate\tnew_todo\n"));
|
assert!(syms.contains("handle_form\tcreate\tnew_todo\n"));
|
||||||
assert!(syms.contains("handle_param\tdelete\ttodo_id\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);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user