use std::collections::HashMap;
use std::rc::Rc;
use sycamore::prelude::*;
use sycamore::web::{console_warn, ViewHtmlNode, ViewNode};
use crate::{BodyRes, Event, FromMd};
type MdComponentProps = (Vec<(String, String)>, Option<Children>);
type MdComponent = Rc<dyn Fn(MdComponentProps) -> View + 'static>;
fn into_type_erased_component<F, Props>(
name: &'static str,
f: F,
) -> impl Fn(MdComponentProps) -> View
where
F: Fn(Props) -> View,
Props: FromMd,
{
move |(props_serialized, children)| {
let mut props = Props::new_prop_default();
for (prop, value) in props_serialized {
if let Err(err) = props.set_prop(&prop, &value) {
console_warn!(
"error setting prop `{prop}` with value `{value}` on `{name}`: {err}"
);
}
}
if let Some(children) = children {
props.set_children(children);
}
f(props)
}
}
#[derive(Default, Clone)]
pub struct ComponentMap {
map: HashMap<&'static str, MdComponent>,
}
impl ComponentMap {
pub fn new() -> Self {
Self::default()
}
pub fn with<F, Props>(mut self, name: &'static str, f: F) -> Self
where
F: Fn(Props) -> View + 'static,
Props: FromMd,
{
self.map
.insert(name, Rc::new(into_type_erased_component(name, f)));
self
}
}
#[derive(Props)]
pub struct MdSycXProps {
body: BodyRes,
#[prop(default)]
components: ComponentMap,
}
#[component]
pub fn MDSycX(props: MdSycXProps) -> View {
let events = props.body.events;
events_to_view(events, props.components)
}
fn events_to_view(events: Vec<Event>, components: ComponentMap) -> View {
let mut fragments_stack: Vec<Vec<View>> = vec![Vec::new()];
let mut attr_stack: Vec<Vec<(String, String)>> = vec![Vec::new()];
let mut element_stack = Vec::new();
let mut events = events.into_iter();
while let Some(ev) = events.next() {
match ev {
Event::Start(tag) => {
if let Some(component) = components.map.get(tag.as_str()).cloned() {
let mut children_events = Vec::new();
let mut component_attributes = Vec::new();
let mut depth = 1;
loop {
let Some(ev) = events.next() else {
console_warn!("tags are not balanced");
break;
};
match &ev {
Event::Start(_) => depth += 1,
Event::End => depth -= 1,
Event::Attr(name, value) if depth == 1 => {
component_attributes.push((name.clone(), value.clone()))
}
_ => {}
}
if depth == 0 {
break;
}
else if !(matches!(ev, Event::Attr(_, _)) && depth == 1) {
children_events.push(ev);
}
}
let components = components.clone();
let children = if !children_events.is_empty() {
Some(Children::new(move || {
events_to_view(children_events, components)
}))
} else {
None
};
let view = component((component_attributes, children));
fragments_stack
.last_mut()
.expect("should always have at least one fragment on stack")
.push(view);
} else {
fragments_stack.push(Vec::new());
attr_stack.push(Vec::new());
element_stack.push(tag);
}
}
Event::End => {
let tag = element_stack.pop().expect("events are not balanced");
let mut node = sycamore::web::HtmlNode::create_element(tag.into());
let children = fragments_stack.pop().expect("events are not balanced");
node.append_view(children.into());
let attributes = attr_stack.pop().expect("events are not balanced");
for (name, value) in attributes {
node.set_attribute(name.into(), value.into());
}
fragments_stack
.last_mut()
.expect("should always have at least one fragment on stack")
.push(node.into());
}
Event::Attr(name, value) => {
attr_stack
.last_mut()
.expect("cannot set attributes without an element")
.push((name, value));
}
Event::Text(text) => {
let node: View = text.into();
fragments_stack
.last_mut()
.expect("should always have at least one fragment on stack")
.push(node)
}
}
}
if fragments_stack.len() != 1 {
panic!("fragment stack is not balanced");
}
fragments_stack.into_iter().next().unwrap().into()
}