designtime-jsx 1.0.5

Lightweight Rust parser for JSX-style HTML and custom components - built for the DesignTime language.
Documentation
use tl::{Node, Parser, ParserOptions};

#[derive(Debug)]
pub enum RenderNode {
    Element {
        tag_name: String,
        attrs: Vec<(String, String)>,
        children: Vec<RenderNode>,
    },
    Text(String),
    Expr(String),
}

fn parse_text_expr(text: &str) -> Vec<RenderNode> {
    let mut nodes = Vec::new();
    let mut buffer = String::new();
    let mut in_expr = false;

    for c in text.chars() {
        match (c, in_expr) {
            ('{', false) => {
                if !buffer.is_empty() {
                    nodes.push(RenderNode::Text(buffer.clone()));
                    buffer.clear();
                }
                in_expr = true;
            }
            ('}', true) => {
                nodes.push(RenderNode::Expr(buffer.clone()));
                buffer.clear();
                in_expr = false;
            }
            _ => buffer.push(c),
        }
    }

    if !buffer.is_empty() {
        nodes.push(if in_expr {
            RenderNode::Expr(buffer)
        } else {
            RenderNode::Text(buffer)
        });
    }

    nodes
}

fn build_ast(node: &Node, parser: &Parser) -> Option<RenderNode> {
    match node {
        Node::Tag(tag) => {
            let tag_name = tag.name().as_utf8_str().to_string();
            
            // Skip artificial text-group elements
            if tag_name == "text-group" {
                return tag.children()
                    .all(parser)
                    .iter()
                    .filter_map(|child| build_ast(child, parser))
                    .next(); // Just take the first child
            }

            let attrs = tag
                .attributes()
                .iter()
                .filter_map(|(k, v)| v.map(|v| (k.as_ref().to_string(), v.as_ref().to_string())))
                .collect();

            let mut children = Vec::new();
            let mut text_buffer = String::new();

            for child in tag.children().all(parser) {
                match build_ast(child, parser) {
                    Some(RenderNode::Text(text)) => {
                        text_buffer.push_str(&text);
                    }
                    Some(node) => {
                        if !text_buffer.is_empty() {
                            children.push(RenderNode::Text(std::mem::take(&mut text_buffer)));
                        }
                        children.push(node);
                    }
                    None => {}
                }
            }

            if !text_buffer.is_empty() {
                children.push(RenderNode::Text(text_buffer));
            }

            Some(RenderNode::Element {
                tag_name,
                attrs,
                children,
            })
        }
        Node::Raw(bytes) => {
            let text = String::from_utf8_lossy(bytes.as_bytes()).trim().to_string();
            if text.is_empty() {
                None
            } else {
                let parts = parse_text_expr(&text);
                match parts.len() {
                    0 => None,
                    1 => Some(parts.into_iter().next().unwrap()),
                    _ => {
                        let mut children = Vec::new();
                        let mut current_text = String::new();
                        
                        for part in parts {
                            match part {
                                RenderNode::Text(text) => current_text.push_str(&text),
                                RenderNode::Expr(expr) => {
                                    if !current_text.is_empty() {
                                        children.push(RenderNode::Text(std::mem::take(&mut current_text)));
                                    }
                                    children.push(RenderNode::Expr(expr));
                                }
                                _ => {}
                            }
                        }
                        
                        if !current_text.is_empty() {
                            children.push(RenderNode::Text(current_text));
                        }
                        
                        match children.len() {
                            0 => None,
                            1 => Some(children.into_iter().next().unwrap()),
                            _ => Some(RenderNode::Element {
                                tag_name: "fragment".to_string(),
                                attrs: Vec::new(),
                                children,
                            }),
                        }
                    }
                }
            }
        }
        Node::Comment(_) => None,
    }
}

pub fn parse_render_block(html: &str) -> Result<Vec<RenderNode>, Box<dyn std::error::Error>> {
    let dom = tl::parse(html, ParserOptions::default())?;
    let parser = dom.parser();

    let children = dom
        .nodes()
        .iter()
        .filter_map(|node| build_ast(node, parser))
        .collect();

    Ok(children)
}