Files
el/ui/vessels/el-platform/src/node.rs
T

159 lines
4.7 KiB
Rust

//! Platform-agnostic node tree.
//!
//! `PlatformNode` is the universal representation of a UI element.
//! Each backend converts this to its native equivalent.
/// An attribute on a platform node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute {
pub name: String,
pub value: String,
}
impl Attribute {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self { name: name.into(), value: value.into() }
}
}
/// A boxed event handler function.
/// Using `Box<dyn Fn(String)>` so platform nodes can store handlers without
/// knowing the native event type.
pub type EventHandler = Box<dyn Fn(String) + Send + Sync>;
/// The kind of a platform node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformNodeKind {
/// An element node: `<div>`, `<button>`, etc.
Element { tag: String },
/// A plain text node.
Text { content: String },
/// A component boundary marker (used for patch reconciliation).
Component { name: String },
/// A fragment — groups children without a wrapper element.
Fragment,
}
/// A platform-agnostic UI node.
///
/// This is the universal intermediate representation. Each backend renders
/// `PlatformNode` trees to its native format.
#[derive(Debug)]
pub struct PlatformNode {
pub kind: PlatformNodeKind,
pub attributes: Vec<Attribute>,
pub children: Vec<PlatformNode>,
/// Opaque platform handle — the backend stores its native pointer/reference
/// here after mounting. `None` before mount.
pub native_handle: Option<usize>,
}
impl PlatformNode {
/// Create an element node.
pub fn element(tag: impl Into<String>) -> Self {
Self {
kind: PlatformNodeKind::Element { tag: tag.into() },
attributes: Vec::new(),
children: Vec::new(),
native_handle: None,
}
}
/// Create a text node.
pub fn text(content: impl Into<String>) -> Self {
Self {
kind: PlatformNodeKind::Text { content: content.into() },
attributes: Vec::new(),
children: Vec::new(),
native_handle: None,
}
}
/// Create a fragment node.
pub fn fragment() -> Self {
Self {
kind: PlatformNodeKind::Fragment,
attributes: Vec::new(),
children: Vec::new(),
native_handle: None,
}
}
/// Add an attribute.
pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.push(Attribute::new(name, value));
self
}
/// Add a child node.
pub fn with_child(mut self, child: PlatformNode) -> Self {
self.children.push(child);
self
}
/// Get the tag name if this is an element node.
pub fn tag(&self) -> Option<&str> {
match &self.kind {
PlatformNodeKind::Element { tag } => Some(tag),
_ => None,
}
}
/// Get the text content if this is a text node.
pub fn text_content(&self) -> Option<&str> {
match &self.kind {
PlatformNodeKind::Text { content } => Some(content),
_ => None,
}
}
/// Render the node tree to an HTML string.
/// This is used by the server backend and for testing all backends.
pub fn to_html(&self) -> String {
match &self.kind {
PlatformNodeKind::Text { content } => html_escape(content),
PlatformNodeKind::Fragment => {
self.children.iter().map(|c| c.to_html()).collect()
}
PlatformNodeKind::Component { name } => {
format!("<!-- component:{} -->", name)
}
PlatformNodeKind::Element { tag } => {
let mut out = format!("<{}", tag);
for attr in &self.attributes {
out.push_str(&format!(" {}=\"{}\"", attr.name, html_escape_attr(&attr.value)));
}
// Void elements — no closing tag
if is_void_element(tag) {
out.push_str(" />");
return out;
}
out.push('>');
for child in &self.children {
out.push_str(&child.to_html());
}
out.push_str(&format!("</{}>", tag));
out
}
}
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
fn html_escape_attr(s: &str) -> String {
html_escape(s).replace('"', "&quot;")
}
fn is_void_element(tag: &str) -> bool {
matches!(
tag,
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input"
| "link" | "meta" | "param" | "source" | "track" | "wbr"
)
}